AMBARI-21382 Log Search UI: implement timezone customization. (ababiichuk)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/b7edc6cf Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/b7edc6cf Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/b7edc6cf Branch: refs/heads/branch-feature-logsearch-ui Commit: b7edc6cf8db108aa5b703817870a8d077c2e7180 Parents: ed66094 Author: ababiichuk <ababiic...@hortonworks.com> Authored: Fri Jun 30 16:29:49 2017 +0300 Committer: ababiichuk <ababiic...@hortonworks.com> Committed: Fri Jun 30 16:29:49 2017 +0300 ---------------------------------------------------------------------- .../ambari-logsearch-web-new/package.json | 2 + .../src/app/app.module.ts | 14 ++- .../dropdown-list/dropdown-list.component.less | 2 +- .../filter-button.component.spec.ts | 75 ++++++++++++++++ .../filter-button/filter-button.component.ts | 91 ++++++++++++++++++++ .../filter-dropdown.component.html | 2 +- .../filter-dropdown.component.spec.ts | 84 +++++++++--------- .../filter-dropdown.component.ts | 65 +++++++++++--- .../filter-text-field.component.html | 3 +- .../filter-text-field.component.spec.ts | 25 +++++- .../filter-text-field.component.ts | 61 +++++++++++-- .../filters-panel/filters-panel.component.html | 51 ++++++----- .../filters-panel.component.spec.ts | 26 ++++-- .../filters-panel/filters-panel.component.ts | 16 +++- .../logs-list/logs-list.component.html | 2 +- .../logs-list/logs-list.component.spec.ts | 11 ++- .../components/logs-list/logs-list.component.ts | 9 +- .../menu-button/menu-button.component.html | 4 +- .../menu-button/menu-button.component.spec.ts | 8 +- .../menu-button/menu-button.component.ts | 23 ++--- .../src/app/models/app-settings.model.ts | 27 ++++++ .../src/app/models/store.model.ts | 53 ++++++++++-- .../src/app/services/filtering.service.spec.ts | 12 ++- .../src/app/services/filtering.service.ts | 51 +++++++---- .../services/storage/app-settings.service.ts | 33 +++++++ .../app/services/storage/audit-logs.service.ts | 7 +- .../app/services/storage/bar-graphs.service.ts | 7 +- .../src/app/services/storage/filters.service.ts | 6 +- .../src/app/services/storage/graphs.service.ts | 6 +- .../src/app/services/storage/nodes.service.ts | 6 +- .../services/storage/service-logs.service.ts | 6 +- .../services/storage/user-configs.service.ts | 6 +- .../src/assets/mock-data.ts | 30 ++++--- .../ambari-logsearch-web-new/yarn.lock | 28 +++++- 34 files changed, 658 insertions(+), 194 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/package.json ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/package.json b/ambari-logsearch/ambari-logsearch-web-new/package.json index da44d30..847787b 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/package.json +++ b/ambari-logsearch/ambari-logsearch-web-new/package.json @@ -23,6 +23,8 @@ "@ngrx/store": "^2.2.2", "@ngx-translate/core": "^6.0.1", "@ngx-translate/http-loader": "^0.0.3", + "angular-moment-timezone": "^0.2.1", + "angular2-moment": "^1.4.0", "bootstrap": "^3.3.7", "core-js": "^2.4.1", "font-awesome": "^4.7.0", http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/app.module.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/app.module.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/app.module.ts index a095a97..1e0ebb0 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/app.module.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/app.module.ts @@ -18,19 +18,22 @@ import {BrowserModule} from '@angular/platform-browser'; import {NgModule, CUSTOM_ELEMENTS_SCHEMA, Injector} from '@angular/core'; -import {FormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {HttpModule, Http, XHRBackend, BrowserXhr, ResponseOptions, XSRFStrategy} from '@angular/http'; import {InMemoryBackendService} from 'angular-in-memory-web-api'; import {AlertModule} from 'ngx-bootstrap'; import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; import {TranslateHttpLoader} from '@ngx-translate/http-loader'; import {StoreModule} from '@ngrx/store'; +import {MomentModule} from 'angular2-moment'; +import {MomentTimezoneModule} from 'angular-moment-timezone'; import {environment} from '../environments/environment'; 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 {FilteringService} from '@app/services/filtering.service'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service'; import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service'; import {BarGraphsService, barGraphs} from '@app/services/storage/bar-graphs.service'; @@ -48,6 +51,7 @@ import {FiltersPanelComponent} from '@app/components/filters-panel/filters-panel import {FilterDropdownComponent} from '@app/components/filter-dropdown/filter-dropdown.component'; import {DropdownListComponent} from '@app/components/dropdown-list/dropdown-list.component'; import {FilterTextFieldComponent} from '@app/components/filter-text-field/filter-text-field.component'; +import {FilterButtonComponent} from '@app/components/filter-button/filter-button.component'; import {AccordionPanelComponent} from '@app/components/accordion-panel/accordion-panel.component'; import {LogsListComponent} from '@app/components/logs-list/logs-list.component'; @@ -82,12 +86,14 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR DropdownListComponent, FilterDropdownComponent, FilterTextFieldComponent, + FilterButtonComponent, AccordionPanelComponent, LogsListComponent ], imports: [ BrowserModule, FormsModule, + ReactiveFormsModule, HttpModule, AlertModule.forRoot(), TranslateModule.forRoot({ @@ -98,6 +104,7 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR } }), StoreModule.provideStore({ + appSettings, auditLogs, serviceLogs, barGraphs, @@ -105,12 +112,15 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR nodes, userConfigs, filters - }) + }), + MomentModule, + MomentTimezoneModule ], providers: [ HttpClientService, ComponentActionsService, FilteringService, + AppSettingsService, AuditLogsService, ServiceLogsService, BarGraphsService, http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/dropdown-list/dropdown-list.component.less ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/dropdown-list/dropdown-list.component.less b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/dropdown-list/dropdown-list.component.less index d0f079a..0853883 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/dropdown-list/dropdown-list.component.less +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/dropdown-list/dropdown-list.component.less @@ -19,4 +19,4 @@ :host { max-height: 500px; // TODO get rid of magic number, base on actual design overflow-y: auto; -} \ No newline at end of file +} http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.spec.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.spec.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.spec.ts new file mode 100644 index 0000000..8828390 --- /dev/null +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.spec.ts @@ -0,0 +1,75 @@ +/** + * 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 {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {Http} from '@angular/http'; +import {FormControl, FormGroup} from '@angular/forms'; +import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; +import {TranslateHttpLoader} from '@ngx-translate/http-loader'; +import {StoreModule} from '@ngrx/store'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; +import {ComponentActionsService} from '@app/services/component-actions.service'; +import {FilteringService} from '@app/services/filtering.service'; + +import {FilterButtonComponent} from './filter-button.component'; + +export function HttpLoaderFactory(http: Http) { + return new TranslateHttpLoader(http, 'assets/i18n/', '.json'); +} + +describe('FilterButtonComponent', () => { + let component: FilterButtonComponent; + let fixture: ComponentFixture<FilterButtonComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [FilterButtonComponent], + imports: [ + StoreModule.provideStore({ + appSettings + }), + TranslateModule.forRoot({ + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [Http] + })], + providers: [ + AppSettingsService, + ComponentActionsService, + FilteringService + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterButtonComponent); + component = fixture.componentInstance; + component.filterName = 'f'; + component.form = new FormGroup({ + f: new FormControl() + }); + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.ts new file mode 100644 index 0000000..3da53ca --- /dev/null +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-button/filter-button.component.ts @@ -0,0 +1,91 @@ +/** + * 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 {Component, Input, forwardRef} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, FormGroup} from '@angular/forms'; +import {ComponentActionsService} from '@app/services/component-actions.service'; +import {FilteringService} from '@app/services/filtering.service'; +import {MenuButtonComponent, menuButtonComponentOptions} from '@app/components/menu-button/menu-button.component'; + +@Component(Object.assign({ + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterButtonComponent), + multi: true + } + ] +}, menuButtonComponentOptions, { + selector: 'filter-button', +})) +export class FilterButtonComponent extends MenuButtonComponent implements ControlValueAccessor { + + constructor(protected actions: ComponentActionsService, private filtering: FilteringService) { + super(actions); + } + + ngAfterViewInit() { + const callback = this.customOnChange ? + (value => this.customOnChange(value)) : (() => this.filtering.filteringSubject.next(null)); + this.form.controls[this.filterName].valueChanges.subscribe(callback); + } + + @Input() + filterName: string; + + @Input() + customOnChange: (value: any) => void; + + @Input() + form: FormGroup; + + private onChange: (fn: any) => void; + + readonly isFilter = true; + + get filterInstance(): any { + return this.filtering.filters[this.filterName]; + } + + get value(): any { + return this.filterInstance.selectedValue; + } + + set value(newValue: any) { + if (this.filtering.valueHasChanged(this.filterInstance.selectedValue, newValue)) { + this.filterInstance.selectedValue = newValue; + this.onChange(newValue); + } + } + + writeValue(options: any) { + const value = options && options.value; + if (this.filtering.valueHasChanged(this.filterInstance.selectedValue, value)) { + this.filterInstance.selectedValue = value; + this.filterInstance.selectedLabel = options.label; + } + } + + registerOnChange(callback: any): void { + this.onChange = callback; + } + + registerOnTouched() { + } + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.html ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.html b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.html index 1ac663e..bb7a206 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.html +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.html @@ -20,4 +20,4 @@ {{filterInstance.selectedLabel | translate}} <span class="caret"></span> </button> <ul class="dropdown-menu" [items]="filterInstance.options" [isFilter]="true" - (selectedItemChange)="setSelectedValue($event)"></ul> + (selectedItemChange)="writeValue($event)"></ul> http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts index e0414f3..9f4522b 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts @@ -18,8 +18,11 @@ import {NO_ERRORS_SCHEMA} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Http} from '@angular/http'; +import {FormControl, FormGroup} from '@angular/forms'; import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; import {TranslateHttpLoader} from '@ngx-translate/http-loader'; +import {StoreModule} from '@ngrx/store'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {FilteringService} from '@app/services/filtering.service'; import {FilterDropdownComponent} from './filter-dropdown.component'; @@ -31,16 +34,43 @@ export function HttpLoaderFactory(http: Http) { describe('FilterDropdownComponent', () => { let component: FilterDropdownComponent; let fixture: ComponentFixture<FilterDropdownComponent>; + const filtering = { + filters: { + f: { + options: [ + { + value: 'v0', + label: 'l0' + }, + { + value: 'v1', + label: 'l1' + } + ] + } + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [FilterDropdownComponent], - imports: [TranslateModule.forRoot({ - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [Http] - })], - providers: [FilteringService], + imports: [ + StoreModule.provideStore({ + appSettings + }), + TranslateModule.forRoot({ + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [Http] + }) + ], + providers: [ + AppSettingsService, + { + provide: FilteringService, + useValue: filtering + } + ], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); @@ -49,18 +79,10 @@ describe('FilterDropdownComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(FilterDropdownComponent); component = fixture.componentInstance; - component.filterInstance = { - options: [ - { - value: 'v0', - label: 'l0' - }, - { - value: 'v1', - label: 'l1' - } - ] - }; + component.filterName = 'f'; + component.form = new FormGroup({ + f: new FormControl() + }); fixture.detectChanges(); }); @@ -68,30 +90,4 @@ describe('FilterDropdownComponent', () => { expect(component).toBeTruthy(); }); - describe('should take initial filter values from 1st item', () => { - it('selectedValue', () => { - expect(component.filterInstance.selectedValue).toEqual('v0'); - }); - - it('selectedLabel', () => { - expect(component.filterInstance.selectedLabel).toEqual('l0'); - }); - }); - - describe('#setSelectedValue()', () => { - beforeEach(() => { - component.setSelectedValue({ - value: 'v2', - label: 'l2' - }); - }); - - it('selectedValue', () => { - expect(component.filterInstance.selectedValue).toEqual('v2'); - }); - - it('selectedLabel', () => { - expect(component.filterInstance.selectedLabel).toEqual('l2'); - }); - }); }); http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.ts index faaafcb..84210dc 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-dropdown/filter-dropdown.component.ts @@ -15,36 +15,75 @@ * limitations under the License. */ -import {Component, OnInit, Input} from '@angular/core'; +import {Component, AfterViewInit, Input, forwardRef} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, FormGroup} from '@angular/forms'; import {FilteringService} from '@app/services/filtering.service'; @Component({ selector: 'filter-dropdown', templateUrl: './filter-dropdown.component.html', - styleUrls: ['./filter-dropdown.component.less'] + styleUrls: ['./filter-dropdown.component.less'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterDropdownComponent), + multi: true + } + ] }) -export class FilterDropdownComponent implements OnInit { +export class FilterDropdownComponent implements AfterViewInit, ControlValueAccessor { constructor(private filtering: FilteringService) { } - ngOnInit() { - this.filterInstance.selectedValue = this.filterInstance.options[0].value; - this.filterInstance.selectedLabel = this.filterInstance.options[0].label; + ngAfterViewInit() { + const callback = this.customOnChange ? + (value => this.customOnChange(value)) : (() => this.filtering.filteringSubject.next(null)); + this.form.controls[this.filterName].valueChanges.subscribe(callback); } @Input() - filterInstance: any; + options: any[]; @Input() - options: any[]; + customOnChange: (value: any) => void; + + @Input() + form: FormGroup; + + @Input() + filterName: string; + + private onChange: (fn: any) => void; + + get filterInstance(): any { + return this.filtering.filters[this.filterName]; + } - setSelectedValue(options: any): void { - if (this.filterInstance.selectedValue !== options.value) { - this.filterInstance.selectedValue = options.value; + get value(): any { + return this.filterInstance.selectedValue; + } + + set value(newValue: any) { + if (this.filtering.valueHasChanged(this.filterInstance.selectedValue, newValue)) { + this.filterInstance.selectedValue = newValue; + this.onChange(newValue); + } + } + + writeValue(options: any) { + const value = options && options.value; + if (this.filtering.valueHasChanged(this.filterInstance.selectedValue, value)) { + this.filterInstance.selectedValue = value; this.filterInstance.selectedLabel = options.label; - this.filtering.filteringSubject.next(null); } - }; + } + + registerOnChange(callback: any): void { + this.onChange = callback; + } + + registerOnTouched() { + } } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.html ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.html b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.html index ed3c4ba..8fb7659 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.html +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.html @@ -17,5 +17,6 @@ <div class="input-group"> <span class="input-group-addon">{{filterInstance.label | translate}}</span> - <input type="text" class="form-control" [(ngModel)]="filterInstance.selectedValue" (change)="onValueChange()"> <!-- TODO use ngModelChange with debounce --> + <input type="text" class="form-control" [(ngModel)]="filterInstance.selectedValue" + (ngModelChange)="writeValue($event)"> </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.spec.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.spec.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.spec.ts index e4f026c..740593f 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.spec.ts @@ -18,9 +18,11 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Http} from '@angular/http'; -import {FormsModule} from '@angular/forms'; +import {FormsModule, FormControl, FormGroup} from '@angular/forms'; import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; import {TranslateHttpLoader} from '@ngx-translate/http-loader'; +import {StoreModule} from '@ngrx/store'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {FilteringService} from '@app/services/filtering.service'; import {FilterTextFieldComponent} from './filter-text-field.component'; @@ -32,6 +34,11 @@ export function HttpLoaderFactory(http: Http) { describe('FilterTextFieldComponent', () => { let component: FilterTextFieldComponent; let fixture: ComponentFixture<FilterTextFieldComponent>; + const filtering = { + filters: { + f: {} + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -42,9 +49,18 @@ describe('FilterTextFieldComponent', () => { provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [Http] + }), + StoreModule.provideStore({ + appSettings }) ], - providers: [FilteringService], + providers: [ + AppSettingsService, + { + provide: FilteringService, + useValue: filtering + } + ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) .compileComponents(); @@ -53,7 +69,10 @@ describe('FilterTextFieldComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(FilterTextFieldComponent); component = fixture.componentInstance; - component.filterInstance = {}; + component.filterName = 'f'; + component.form = new FormGroup({ + f: new FormControl() + }); fixture.detectChanges(); }); http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.ts index 3f23ffd..857b511 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filter-text-field/filter-text-field.component.ts @@ -15,27 +15,74 @@ * limitations under the License. */ -import {Component, OnInit, Input} from '@angular/core'; +import {Component, AfterViewInit, Input, forwardRef} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, FormGroup} from '@angular/forms'; +import 'rxjs/add/operator/debounceTime'; import {FilteringService} from '@app/services/filtering.service'; @Component({ selector: 'filter-text-field', templateUrl: './filter-text-field.component.html', - styleUrls: ['./filter-text-field.component.less'] + styleUrls: ['./filter-text-field.component.less'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterTextFieldComponent), + multi: true + } + ] }) -export class FilterTextFieldComponent implements OnInit { +export class FilterTextFieldComponent implements AfterViewInit, ControlValueAccessor { constructor(private filtering: FilteringService) { } - ngOnInit() { + ngAfterViewInit() { + const callback = this.customOnChange ? + (value => this.customOnChange(value)) : (() => this.filtering.filteringSubject.next(null)); + this.form.controls[this.filterName].valueChanges.debounceTime(this.debounceInterval).subscribe(callback); } @Input() - filterInstance: any; + filterName: string; - onValueChange() { - this.filtering.filteringSubject.next(null); + @Input() + customOnChange: (value: any) => void; + + @Input() + form: FormGroup; + + private onChange: (fn: any) => void; + + private readonly debounceInterval = 1500; + + get filterInstance(): any { + return this.filtering.filters[this.filterName]; + } + + get value(): any { + return this.filterInstance.selectedValue; + } + + set value(newValue: any) { + if (this.filtering.valueHasChanged(this.filterInstance.selectedValue, newValue)) { + this.filterInstance.selectedValue = newValue; + this.onChange(newValue); + } + } + + writeValue(options: any) { + const value = options && options.value; + if (this.filtering.valueHasChanged(this.filterInstance.selectedValue, value)) { + this.filterInstance.selectedValue = value; + } + } + + registerOnChange(callback: any): void { + this.onChange = callback; + } + + registerOnTouched() { } } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.html ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.html b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.html index e278a09..256b547 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.html +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.html @@ -15,24 +15,33 @@ limitations under the License. --> -<div class="form-inline filter-input-container col-md-8"> - <filter-dropdown [filterInstance]="filters.clusters"></filter-dropdown> - <filter-text-field [filterInstance]="filters.text"></filter-text-field> - <filter-dropdown [filterInstance]="filters.timeRange"></filter-dropdown> - <filter-dropdown [filterInstance]="filters.timeZone"></filter-dropdown> - <!--button class="btn btn-success" type="button"> - <span class="fa fa-search"></span> - </button--> -</div> -<div class="default-flex col-md-4"> - <a href="#"> - <span class="fa fa-search-minus"></span> {{'filter.excluded' | translate}} - </a> - <menu-button [label]="filters.components.label" [iconClass]="filters.components.iconClass" - [subItems]="filters.components.options" [isFilter]="true" - [filterInstance]="filters.components"></menu-button> - <menu-button [label]="filters.levels.label" [iconClass]="filters.levels.iconClass" - [subItems]="filters.levels.options" [isFilter]="true" - [filterInstance]="filters.levels"></menu-button> - <menu-button label="filter.capture" iconClass="fa fa-caret-right"></menu-button> -</div> +<form [formGroup]="filtersForm"> + <div class="form-inline filter-input-container col-md-8"> + <filter-dropdown [(ngModel)]="filters.clusters.selectedValue" [filterName]="'clusters'" + formControlName="clusters" [form]="filtersForm"></filter-dropdown> + <filter-text-field [(ngModel)]="filters.text.selectedValue" [filterName]="'text'" formControlName="text" + [form]="filtersForm"></filter-text-field> + <filter-dropdown [(ngModel)]="filters.timeRange.selectedValue" [filterName]="'timeRange'" + formControlName="timeRange" [form]="filtersForm"></filter-dropdown> + <filter-dropdown [(ngModel)]="filters.timeZone.selectedValue" [filterName]="'timeZone'" + formControlName="timeZone" [form]="filtersForm" + [customOnChange]="setTimeZone"></filter-dropdown> + <!--button class="btn btn-success" type="button"> + <span class="fa fa-search"></span> + </button--> + </div> + <div class="default-flex col-md-4"> + <a href="#"> + <span class="fa fa-search-minus"></span> {{'filter.excluded' | translate}} + </a> + <filter-button [(ngModel)]="filters.components.selectedValue" formControlName="components" + [label]="filters.components.label" [iconClass]="filters.components.iconClass" + [subItems]="filters.components.options" [filterName]="'components'" + [form]="filtersForm"></filter-button> + <filter-button [(ngModel)]="filters.levels.selectedValue" formControlName="levels" + [label]="filters.levels.label" [iconClass]="filters.levels.iconClass" + [subItems]="filters.levels.options" [filterName]="'levels'" + [form]="filtersForm"></filter-button> + <menu-button label="filter.capture" iconClass="fa fa-caret-right"></menu-button> + </div> +</form> http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.spec.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.spec.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.spec.ts index 934f37a..8291572 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.spec.ts @@ -16,11 +16,13 @@ * limitations under the License. */ -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Http} from '@angular/http'; import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; import {TranslateHttpLoader} from '@ngx-translate/http-loader'; +import {StoreModule} from '@ngrx/store'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {FilteringService} from '@app/services/filtering.service'; import {FiltersPanelComponent} from './filters-panel.component'; @@ -36,13 +38,21 @@ describe('FiltersPanelComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [FiltersPanelComponent], - imports: [TranslateModule.forRoot({ - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [Http] - })], - providers: [FilteringService], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + imports: [ + StoreModule.provideStore({ + appSettings + }), + TranslateModule.forRoot({ + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [Http] + }) + ], + providers: [ + AppSettingsService, + FilteringService + ], + schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); })); http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.ts index f7b8429..b2ae9b0 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/filters-panel/filters-panel.component.ts @@ -17,7 +17,9 @@ */ import {Component, OnInit} from '@angular/core'; +import {FormControl, FormGroup} from '@angular/forms'; import {FilteringService} from '@app/services/filtering.service'; +import {AppSettingsService} from '@app/services/storage/app-settings.service'; @Component({ selector: 'filters-panel', @@ -26,7 +28,7 @@ import {FilteringService} from '@app/services/filtering.service'; }) export class FiltersPanelComponent implements OnInit { - constructor(private filtering: FilteringService) { + constructor(private filtering: FilteringService, private appSettings: AppSettingsService) { } ngOnInit() { @@ -36,4 +38,16 @@ export class FiltersPanelComponent implements OnInit { return this.filtering.filters; } + private filtersFormItems = Object.keys(this.filters).reduce((currentObject, key) => { + let item = {}; + item[key] = new FormControl(); + return Object.assign(currentObject, item); + }, {}); + + filtersForm = new FormGroup(this.filtersFormItems); + + setTimeZone(timeZone: string): void { + this.appSettings.setParameter('timeZone', timeZone); + } + } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.html ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.html b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.html index 827f63c..df72502 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.html +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.html @@ -21,7 +21,7 @@ <div [ngClass]="'col-md-1 log-status ' + log.className">{{log.level}}</div> <div class="col-md-3"> <div class="log-type">{{log.type}}</div> - <time class="log-time">{{log.time}}</time> + <time class="log-time">{{log.time | amTz: filtering.timeZone | amDateFormat: timeFormat}}</time> </div> <div class="col-md-6 log-content-wrapper"> <div class="collapse log-actions" [attr.id]="'details-' + i"> http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.spec.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.spec.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.spec.ts index 072bfcd..ea3d780 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.spec.ts @@ -18,8 +18,11 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {StoreModule} from '@ngrx/store'; +import {MomentModule} from 'angular2-moment'; +import {MomentTimezoneModule} from 'angular-moment-timezone'; import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service'; import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {HttpClientService} from '@app/services/http-client.service'; import {FilteringService} from '@app/services/filtering.service'; @@ -43,8 +46,11 @@ describe('LogsListComponent', () => { imports: [ StoreModule.provideStore({ auditLogs, - serviceLogs - }) + serviceLogs, + appSettings + }), + MomentModule, + MomentTimezoneModule ], providers: [ { @@ -53,6 +59,7 @@ describe('LogsListComponent', () => { }, AuditLogsService, ServiceLogsService, + AppSettingsService, FilteringService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.ts index cc77784..efa05e4 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/logs-list/logs-list.component.ts @@ -19,6 +19,7 @@ import {Component, OnInit, Input} from '@angular/core'; import 'rxjs/add/operator/map'; import {HttpClientService} from '@app/services/http-client.service'; import {ServiceLogsService} from '@app/services/storage/service-logs.service'; +import {AppSettingsService} from '@app/services/storage/app-settings.service'; import {FilteringService} from '@app/services/filtering.service'; @Component({ @@ -28,7 +29,7 @@ import {FilteringService} from '@app/services/filtering.service'; }) export class LogsListComponent implements OnInit { - constructor(private httpClient: HttpClientService, private serviceLogsStorage: ServiceLogsService, private filtering: FilteringService) { + constructor(private httpClient: HttpClientService, private serviceLogsStorage: ServiceLogsService, private appSettings: AppSettingsService, private filtering: FilteringService) { this.filtering.filteringSubject.subscribe(this.loadLogs.bind(this)); } @@ -39,6 +40,8 @@ export class LogsListComponent implements OnInit { @Input() private logsArrayId: string; + timeFormat: string = 'DD/MM/YYYY HH:mm:ss'; + private readonly usedFilters = { clusters: ['clusters'], text: ['iMessage'], @@ -47,14 +50,14 @@ export class LogsListComponent implements OnInit { levels: ['level'] }; - logs = this.serviceLogsStorage.getInstances().map(logs => { + logs = this.serviceLogsStorage.getAll().map(logs => { return logs.map(log => { return { type: log.type, level: log.level, className: log.level.toLowerCase(), message: log.log_message, - time: new Date(log.logtime).toLocaleDateString() + ' ' + new Date(log.logtime).toLocaleTimeString() // TODO use moment with custom time zone + time: log.logtime } }); }); http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.html ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.html b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.html index 132d717..5111197 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.html +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.html @@ -22,6 +22,6 @@ <br> <a *ngIf="label" (mousedown)="onMouseDown($event)" [ngClass]="labelClass" (mouseup)="onMouseUp($event)" (click)="$event.stopPropagation()">{{label | translate}}</a> - <ul class="dropdown-menu" [isFilter]="isFilter" *ngIf="hasSubItems" [items]="subItems" - (selectedItemChange)="setSelectedValue($event)"></ul> + <ul class="dropdown-menu" *ngIf="hasSubItems" [items]="subItems" [isFilter]="isFilter" + (selectedItemChange)="isFilter && writeValue($event)"></ul> </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.spec.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.spec.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.spec.ts index 424d322..d53677d 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.spec.ts @@ -39,10 +39,10 @@ describe('MenuButtonComponent', () => { declarations: [MenuButtonComponent], imports: [ TranslateModule.forRoot({ - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [Http] - })], + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [Http] + })], providers: [ ComponentActionsService, FilteringService http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.ts index ded01b7..08b555c 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/components/menu-button/menu-button.component.ts @@ -18,17 +18,18 @@ import {Component, AfterViewInit, Input, ViewChild, ElementRef} from '@angular/core'; import {ComponentActionsService} from '@app/services/component-actions.service'; -import {FilteringService} from '@app/services/filtering.service'; import * as $ from 'jquery'; -@Component({ +export const menuButtonComponentOptions = { selector: 'menu-button', templateUrl: './menu-button.component.html', styleUrls: ['./menu-button.component.less'] -}) +} + +@Component(menuButtonComponentOptions) export class MenuButtonComponent implements AfterViewInit { - constructor(private actions: ComponentActionsService, private filtering: FilteringService) { + constructor(protected actions: ComponentActionsService) { } ngAfterViewInit() { @@ -43,11 +44,7 @@ export class MenuButtonComponent implements AfterViewInit { @Input() action: string; - @Input() - isFilter: boolean; - - @Input() - filterInstance?: any; + isFilter: boolean = false; @Input() iconClass: string; @@ -91,12 +88,4 @@ export class MenuButtonComponent implements AfterViewInit { } } - setSelectedValue(options: any): void { - if (this.filterInstance.selectedValue !== options.value) { - this.filterInstance.selectedValue = options.value; - this.filterInstance.selectedLabel = options.label; - this.filtering.filteringSubject.next(null); - } - }; - } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/models/app-settings.model.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/models/app-settings.model.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/models/app-settings.model.ts new file mode 100644 index 0000000..30bf0c8 --- /dev/null +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/models/app-settings.model.ts @@ -0,0 +1,27 @@ +/** + * 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 * as moment from 'moment-timezone'; + +export interface AppSettings { + timeZone: string; +} + +export const defaultSettings: AppSettings = { + timeZone: moment.tz.guess() +} http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/models/store.model.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/models/store.model.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/models/store.model.ts index 1f3a89c..c169c1b 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/models/store.model.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/models/store.model.ts @@ -16,6 +16,7 @@ * limitations under the License. */ +import {AppSettings} from '@app/models/app-settings.model'; import {Observable} from 'rxjs/Observable'; import {Store, Action} from '@ngrx/store'; import {AuditLog} from '@app/models/audit-log.model'; @@ -29,10 +30,12 @@ import {Filter} from '@app/models/filter.model'; export const storeActions = { ADD: 'ADD', DELETE: 'DELETE', - CLEAR: 'CLEAR' + CLEAR: 'CLEAR', + SET: 'SET' }; export interface AppStore { + appSettings: AppSettings; auditLogs: AuditLog[]; serviceLogs: ServiceLog[]; barGraphs: BarGraph[]; @@ -44,17 +47,25 @@ export interface AppStore { export class ModelService { - constructor(private modelName: string, private store: Store<AppStore>) {} + constructor(modelName: string, store: Store<AppStore>) { + this.modelName = modelName; + this.store = store; + } + + protected modelName: string; + + protected store: Store<AppStore>; - getInstances(): Observable<any> { + getAll(): Observable<any> { return this.store.select(this.modelName); } +} + +export class CollectionModelService extends ModelService { + addInstance(instance: any): void { - this.store.dispatch({ - type: storeActions.ADD, - payload: [instance] - }); + this.addInstances([instance]); } addInstances(instances: any[]): void { @@ -79,7 +90,24 @@ export class ModelService { } -export function reducer(state: any, action: Action): any { +export class ObjectModelService extends ModelService { + + setParameter(key: string, value: any): void { + let payload = {}; + payload[key] = value; + this.setParameters(payload); + } + + setParameters(params: any): void { + this.store.dispatch({ + type: storeActions.SET, + payload: params + }); + } + +} + +export function collectionReducer(state: any, action: Action): any { switch (action.type) { case storeActions.ADD: return [...state, ...action.payload]; @@ -93,3 +121,12 @@ export function reducer(state: any, action: Action): any { return state; } } + +export function objectReducer(state: any, action: Action): any { + switch (action.type) { + case storeActions.SET: + return Object.assign({}, state, action.payload); + default: + return state; + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.spec.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.spec.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.spec.ts index d471e00..a8dc017 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.spec.ts @@ -17,13 +17,23 @@ */ import {TestBed, inject} from '@angular/core/testing'; +import {StoreModule} from '@ngrx/store'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {FilteringService} from './filtering.service'; describe('FilteringService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [FilteringService] + imports: [ + StoreModule.provideStore({ + appSettings + }) + ], + providers: [ + FilteringService, + AppSettingsService + ] }); }); http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.ts index c4d2bdf..9f6b7dc 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/filtering.service.ts @@ -19,13 +19,17 @@ import {Injectable} from '@angular/core'; import {Subject} from 'rxjs/Subject'; import * as moment from 'moment-timezone'; +import {AppSettingsService} from '@app/services/storage/app-settings.service'; @Injectable() export class FilteringService { - constructor() { + constructor(private appSettings: AppSettingsService) { + this.appSettings.getAll().subscribe(settings => this.timeZone = settings.timeZone); } + timeZone: string; + // TODO implement loading of real options data filters = { clusters: { @@ -57,7 +61,7 @@ export class FilteringService { } ], selectedValue: '', - selectedLabel: '', + selectedLabel: 'filter.all', paramName: 'clusters', }, text: { @@ -133,19 +137,23 @@ export class FilteringService { } } ], - selectedValue: '', - selectedLabel: '' + selectedValue: { + type: 'LAST', + unit: 'h', + interval: 1 + }, + selectedLabel: 'filter.timeRange.1hr' }, timeZone: { options: moment.tz.names().map(zone => { // TODO map labels according to actual design requirements return { - label: `${zone} (${moment.tz(zone).format('Z')})`, + label: this.getTimeZoneLabel(zone), value: zone }; }), - selectedValue: '', - selectedLabel: '' + selectedValue: moment.tz.guess(), + selectedLabel: this.getTimeZoneLabel(moment.tz.guess()) }, components: { label: 'filter.components', @@ -173,7 +181,7 @@ export class FilteringService { } ], selectedValue: '', - selectedLabel: '' + selectedLabel: 'filter.all' }, levels: { label: 'filter.levels', @@ -213,7 +221,7 @@ export class FilteringService { } ], selectedValue: '', - selectedLabel: '' + selectedLabel: 'filter.all' } }; @@ -226,12 +234,10 @@ export class FilteringService { time = moment(); break; case 'CURRENT': - // TODO consider user's timezone - time = moment().endOf(value.unit); + time = moment().tz(this.timeZone).endOf(value.unit); break; case 'PAST': - // TODO consider user's timezone - time = moment().startOf(value.unit).millisecond(-1); + time = moment().tz(this.timeZone).startOf(value.unit).millisecond(-1); break; default: break; @@ -248,11 +254,9 @@ export class FilteringService { time = endTime.subtract(value.interval, value.unit); break; case 'CURRENT': - // TODO consider user's timezone - time = moment().startOf(value.unit); + time = moment().tz(this.timeZone).startOf(value.unit); break; case 'PAST': - // TODO consider user's timezone time = endTime.startOf(value.unit); break; default: @@ -263,6 +267,21 @@ export class FilteringService { } }; + getTimeZoneLabel(timeZone) { + return `${timeZone} (${moment.tz(timeZone).format('Z')})`; + } + + valueHasChanged(currentValue: any, newValue: any): boolean { + if (newValue == null) { + return false; + } + if (typeof newValue === 'object') { + return JSON.stringify(currentValue) !== JSON.stringify(newValue); + } else { + return currentValue !== newValue; + } + } + filteringSubject = new Subject(); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/app-settings.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/app-settings.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/app-settings.service.ts new file mode 100644 index 0000000..1c87a3c --- /dev/null +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/app-settings.service.ts @@ -0,0 +1,33 @@ +/** + * 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 {Action, ActionReducer, Store} from '@ngrx/store'; +import {AppSettings, defaultSettings} from '@app/models/app-settings.model'; +import {AppStore, ObjectModelService, objectReducer} from '@app/models/store.model'; + +@Injectable() +export class AppSettingsService extends ObjectModelService { + constructor(store: Store<AppStore>) { + super('appSettings', store); + } +} + +export const appSettings: ActionReducer<AppSettings> = (state: AppSettings = defaultSettings, action: Action) => { + return objectReducer(state, action); +} http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/audit-logs.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/audit-logs.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/audit-logs.service.ts index 706d2f3..7c322ed 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/audit-logs.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/audit-logs.service.ts @@ -16,19 +16,18 @@ * limitations under the License. */ - import {Injectable} from '@angular/core'; import {Action, ActionReducer, Store} from '@ngrx/store'; import {AuditLog} from '@app/models/audit-log.model'; -import {AppStore, ModelService, reducer} from '@app/models/store.model'; +import {AppStore, CollectionModelService, collectionReducer} from '@app/models/store.model'; @Injectable() -export class AuditLogsService extends ModelService { +export class AuditLogsService extends CollectionModelService { constructor(store: Store<AppStore>) { super('auditLogs', store); } } export const auditLogs: ActionReducer<AuditLog[]> = (state: AuditLog[] = [], action: Action) => { - return reducer(state, action); + return collectionReducer(state, action); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/bar-graphs.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/bar-graphs.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/bar-graphs.service.ts index 2e2db5b..0109409 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/bar-graphs.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/bar-graphs.service.ts @@ -16,19 +16,18 @@ * limitations under the License. */ - import {Injectable} from '@angular/core'; import {Action, ActionReducer, Store} from '@ngrx/store'; import {BarGraph} from '@app/models/bar-graph.model'; -import {AppStore, ModelService, reducer} from '@app/models/store.model'; +import {AppStore, CollectionModelService, collectionReducer} from '@app/models/store.model'; @Injectable() -export class BarGraphsService extends ModelService { +export class BarGraphsService extends CollectionModelService { constructor(store: Store<AppStore>) { super('barGraphs', store); } } export const barGraphs: ActionReducer<BarGraph[]> = (state: BarGraph[] = [], action: Action) => { - return reducer(state, action); + return collectionReducer(state, action); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/filters.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/filters.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/filters.service.ts index 99d2140..b8748e9 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/filters.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/filters.service.ts @@ -20,15 +20,15 @@ import {Injectable} from '@angular/core'; import {Action, ActionReducer, Store} from '@ngrx/store'; import {Filter} from '@app/models/filter.model'; -import {AppStore, ModelService, reducer} from '@app/models/store.model'; +import {AppStore, CollectionModelService, collectionReducer} from '@app/models/store.model'; @Injectable() -export class FiltersService extends ModelService { +export class FiltersService extends CollectionModelService { constructor(store: Store<AppStore>) { super('filters', store); } } export const filters: ActionReducer<Filter[]> = (state: Filter[] = [], action: Action) => { - return reducer(state, action); + return collectionReducer(state, action); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/graphs.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/graphs.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/graphs.service.ts index 1c32d34..eda04ee 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/graphs.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/graphs.service.ts @@ -20,15 +20,15 @@ import {Injectable} from '@angular/core'; import {Action, ActionReducer, Store} from '@ngrx/store'; import {Graph} from '@app/models/graph.model'; -import {AppStore, ModelService, reducer} from '@app/models/store.model'; +import {AppStore, CollectionModelService, collectionReducer} from '@app/models/store.model'; @Injectable() -export class GraphsService extends ModelService { +export class GraphsService extends CollectionModelService { constructor(store: Store<AppStore>) { super('graphs', store); } } export const graphs: ActionReducer<Graph[]> = (state: Graph[] = [], action: Action) => { - return reducer(state, action); + return collectionReducer(state, action); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/nodes.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/nodes.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/nodes.service.ts index b194f94..7b2e6e9 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/nodes.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/nodes.service.ts @@ -20,15 +20,15 @@ import {Injectable} from '@angular/core'; import {Action, ActionReducer, Store} from '@ngrx/store'; import {Node} from '@app/models/node.model'; -import {AppStore, ModelService, reducer} from '@app/models/store.model'; +import {AppStore, CollectionModelService, collectionReducer} from '@app/models/store.model'; @Injectable() -export class NodesService extends ModelService { +export class NodesService extends CollectionModelService { constructor(store: Store<AppStore>) { super('nodes', store); } } export const nodes: ActionReducer<Node[]> = (state: Node[] = [], action: Action) => { - return reducer(state, action); + return collectionReducer(state, action); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/service-logs.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/service-logs.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/service-logs.service.ts index 6859653..ba277c4 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/service-logs.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/service-logs.service.ts @@ -20,15 +20,15 @@ import {Injectable} from '@angular/core'; import {Action, ActionReducer, Store} from '@ngrx/store'; import {ServiceLog} from '@app/models/service-log.model'; -import {AppStore, ModelService, reducer} from '@app/models/store.model'; +import {AppStore, CollectionModelService, collectionReducer} from '@app/models/store.model'; @Injectable() -export class ServiceLogsService extends ModelService { +export class ServiceLogsService extends CollectionModelService { constructor(store: Store<AppStore>) { super('serviceLogs', store); } } export const serviceLogs: ActionReducer<ServiceLog[]> = (state: ServiceLog[] = [], action: Action) => { - return reducer(state, action); + return collectionReducer(state, action); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/user-configs.service.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/user-configs.service.ts b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/user-configs.service.ts index 2c770f0..b26485d 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/user-configs.service.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/app/services/storage/user-configs.service.ts @@ -20,15 +20,15 @@ import {Injectable} from '@angular/core'; import {Action, ActionReducer, Store} from '@ngrx/store'; import {UserConfig} from '@app/models/user-config.model'; -import {AppStore, ModelService, reducer} from '@app/models/store.model'; +import {AppStore, CollectionModelService, collectionReducer} from '@app/models/store.model'; @Injectable() -export class UserConfigsService extends ModelService { +export class UserConfigsService extends CollectionModelService { constructor(store: Store<AppStore>) { super('userConfigs', store); } } export const userConfigs: ActionReducer<UserConfig[]> = (state: UserConfig[] = [], action: Action) => { - return reducer(state, action); + return collectionReducer(state, action); } http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/src/assets/mock-data.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/src/assets/mock-data.ts b/ambari-logsearch/ambari-logsearch-web-new/src/assets/mock-data.ts index d269bc3..732e8d3 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/src/assets/mock-data.ts +++ b/ambari-logsearch/ambari-logsearch-web-new/src/assets/mock-data.ts @@ -16,6 +16,8 @@ * limitations under the License. */ +import * as moment from 'moment-timezone'; + export const mockData = { login: {}, api: { @@ -224,7 +226,7 @@ export const mockData = { path: '/var/log/ambari-metrics-collector/ambari-metrics-collector.log', host: 'h0', level: 'WARN', - logtime: 1497474000000, + logtime: moment().valueOf(), ip: '192.168.0.1', logfile_line_number: 8, type: 'ams_collector', @@ -241,14 +243,14 @@ export const mockData = { event_md5: '1908755391', event_dur_ms: 200, _ttl_: "+5DAYS", - _expire_at_: 1497906000000, + _expire_at_: moment().add(5, 'd').valueOf(), _router_field_: 20 }, { path: '/var/log/ambari-metrics-collector/ambari-metrics-collector.log', host: 'h1', level: 'ERROR', - logtime: 1497387600000, + logtime: moment().subtract(2, 'd'), ip: '192.168.0.2', type: 'ams_collector', _version_: 14, @@ -265,14 +267,14 @@ export const mockData = { event_md5: '1029384756', event_dur_ms: 700, _ttl_: "+5DAYS", - _expire_at_: 1497819600000, + _expire_at_: moment().add(3, 'd').valueOf(), _router_field_: 5 }, { path: '/var/log/ambari-metrics-collector/ambari-metrics-collector.log', host: 'h1', level: 'FATAL', - logtime: 1497042000000, + logtime: moment().subtract(10, 'd').valueOf(), ip: '192.168.0.3', type: 'ambari_agent', _version_: 14, @@ -289,14 +291,14 @@ export const mockData = { event_md5: '67589403', event_dur_ms: 100, _ttl_: "+5DAYS", - _expire_at_: 1497474000000, + _expire_at_: moment().subtract(5, 'd').valueOf(), _router_field_: 45 }, { path: '/var/log/ambari-metrics-collector/zookeeper-server.log', host: 'h1', level: 'INFO', - logtime: 1497956919700, + logtime: moment().subtract(25, 'h').valueOf(), ip: '192.168.0.4', type: 'zookeeper_server', _version_: 14, @@ -313,14 +315,14 @@ export const mockData = { event_md5: '67589403', event_dur_ms: 1000, _ttl_: "+5DAYS", - _expire_at_: 1497956939700, + _expire_at_: moment().subtract(25, 'h').add(5, 'd').valueOf(), _router_field_: 55 }, { path: '/var/log/ambari-metrics-collector/zookeeper-server.log', host: 'h1', level: 'DEBUG', - logtime: 1497956919700, + logtime: moment().subtract(25, 'd').valueOf(), ip: '192.168.0.4', type: 'zookeeper_server', _version_: 14, @@ -337,14 +339,14 @@ export const mockData = { event_md5: '67589403', event_dur_ms: 1000, _ttl_: "+5DAYS", - _expire_at_: 1497956939700, + _expire_at_: moment().subtract(20, 'd').valueOf(), _router_field_: 55 }, { path: '/var/log/ambari-metrics-collector/zookeeper-client.log', host: 'h1', level: 'TRACE', - logtime: 1497956919700, + logtime: moment().subtract(2, 'h').valueOf(), ip: '192.168.0.4', type: 'zookeeper_client', _version_: 14, @@ -361,14 +363,14 @@ export const mockData = { event_md5: '67589403', event_dur_ms: 1000, _ttl_: "+5DAYS", - _expire_at_: 1497956939700, + _expire_at_: moment().subtract(2, 'h').add(5, 'd').valueOf(), _router_field_: 55 }, { path: '/var/log/ambari-metrics-collector/zookeeper-client.log', host: 'h1', level: 'UNKNOWN', - logtime: 1497956919700, + logtime: moment().subtract(31, 'd').valueOf(), ip: '192.168.0.4', type: 'zookeeper_client', _version_: 14, @@ -385,7 +387,7 @@ export const mockData = { event_md5: '67589403', event_dur_ms: 1000, _ttl_: "+5DAYS", - _expire_at_: 1497956939700, + _expire_at_: moment().subtract(26, 'd').valueOf(), _router_field_: 55 } ], http://git-wip-us.apache.org/repos/asf/ambari/blob/b7edc6cf/ambari-logsearch/ambari-logsearch-web-new/yarn.lock ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web-new/yarn.lock b/ambari-logsearch/ambari-logsearch-web-new/yarn.lock index ff37374..6e592a7 100644 --- a/ambari-logsearch/ambari-logsearch-web-new/yarn.lock +++ b/ambari-logsearch/ambari-logsearch-web-new/yarn.lock @@ -240,6 +240,32 @@ angular-in-memory-web-api@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/angular-in-memory-web-api/-/angular-in-memory-web-api-0.3.2.tgz#8836d9e2534d37b728f3cb5a1caf6fe1e7fbbecd" +angular-moment-timezone@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/angular-moment-timezone/-/angular-moment-timezone-0.2.1.tgz#b2c1d9dd0e90558483b4da8db277bee4dbdbf6d1" + dependencies: + "@angular/common" "^4.0.0" + "@angular/compiler" "^4.0.0" + "@angular/core" "^4.0.0" + "@angular/forms" "^4.0.0" + "@angular/http" "^4.0.0" + "@angular/platform-browser" "^4.0.0" + "@angular/platform-browser-dynamic" "^4.0.0" + "@angular/router" "^4.0.0" + "@types/moment-timezone" "^0.2.34" + angular2-moment "^1.3.3" + core-js "^2.4.1" + moment "^2.18.1" + moment-timezone "^0.5.13" + rxjs "^5.1.0" + zone.js "^0.8.4" + +angular2-moment@^1.3.3, angular2-moment@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/angular2-moment/-/angular2-moment-1.4.0.tgz#3d59c1ebc28934fcfe9b888ab461e261724987e8" + dependencies: + moment "^2.16.0" + ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -3045,7 +3071,7 @@ moment-timezone@^0.5.13: dependencies: moment ">= 2.9.0" -moment@*, moment@2.18.1, "moment@>= 2.9.0", moment@>=2.14.0, moment@^2.18.1: +moment@*, moment@2.18.1, "moment@>= 2.9.0", moment@>=2.14.0, moment@^2.16.0, moment@^2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"