- 
                Notifications
    You must be signed in to change notification settings 
- Fork 11
Page level time range #1441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Page level time range #1441
Changes from all commits
dd20f13
              a8523ab
              4bfa6d6
              63a665e
              b81ae1a
              6e84fbe
              ab466fe
              3043f45
              56c574b
              c1be553
              8eb3a52
              ffe3d6b
              3f98a99
              f0d9ffe
              e1bea69
              b359fcd
              ab36992
              cd870bc
              7a92d50
              b241e0e
              7e279e4
              10e8225
              1738b10
              c44b03b
              bd1cbbd
              3adc661
              6e78e54
              c225cf9
              c2d0287
              7bc71c7
              b4b4722
              a528987
              63c4bd0
              8742a2b
              b795d39
              9ed2b22
              67c8bfe
              0854000
              62d1041
              5037200
              11b0991
              50153f9
              12ab84d
              ea6357a
              b558d41
              0989a6a
              3845357
              a37a477
              00f785a
              82a9612
              9714df8
              4b0b74e
              44a67b6
              bfcb9b8
              38c5e82
              efd4d15
              5e5dcf5
              7172091
              505d5d7
              748743e
              09cf639
              5c0cfe6
              541555f
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| import { InjectionToken } from '@angular/core'; | ||
|  | ||
| export const GLOBAL_HEADER_HEIGHT = new InjectionToken<string>('Global Header Height'); | ||
|  | ||
| export const enum ApplicationFeature { | ||
| PageTimeRange = 'ui.page-time-range' | ||
| } | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| import { Observable } from 'rxjs'; | ||
| import { TimeRange } from '../time/time-range'; | ||
| import { Breadcrumb } from './breadcrumb'; | ||
|  | ||
| export interface HtRouteData { | ||
| breadcrumb?: Breadcrumb | Observable<Breadcrumb>; | ||
| features?: string[]; | ||
| title?: string; | ||
| defaultTimeRange?: TimeRange; | ||
| } | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { | ||
| FeatureState, | ||
| FeatureStateResolver, | ||
| FixedTimeRange, | ||
| NavigationService, | ||
| RelativeTimeRange, | ||
| TimeDuration, | ||
| TimeRange, | ||
| TimeRangeService, | ||
| TimeUnit | ||
| } from '@hypertrace/common'; | ||
| import { runFakeRxjs } from '@hypertrace/test-utils'; | ||
| import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; | ||
| import { of } from 'rxjs'; | ||
| import { map } from 'rxjs/operators'; | ||
| import { PageTimeRangePreferenceService } from './page-time-range-preference.service'; | ||
|  | ||
| describe('Page time range preference service', () => { | ||
| const defaultPageTimeRange = new RelativeTimeRange(new TimeDuration(2, TimeUnit.Hour)); | ||
| const serviceFactory = createServiceFactory({ | ||
| service: PageTimeRangePreferenceService, | ||
| providers: [ | ||
| mockProvider(NavigationService, { | ||
| getCurrentActivatedRoute: jest | ||
| .fn() | ||
| .mockReturnValue({ snapshot: { data: { defaultTimeRange: defaultPageTimeRange } } }) | ||
| }), | ||
| mockProvider(FeatureStateResolver, { | ||
| getFeatureState: jest.fn().mockReturnValue(of(FeatureState.Enabled)) | ||
| }) | ||
| ] | ||
| }); | ||
|  | ||
| test('Setting fixed time range emits corresponding time range from preferences', () => { | ||
| runFakeRxjs(({ expectObservable, cold }) => { | ||
| const timeRange: TimeRange = new FixedTimeRange(new Date(1573255100253), new Date(1573255111159)); | ||
| const spectator = serviceFactory({ | ||
| providers: [ | ||
| mockProvider(TimeRangeService, { | ||
| timeRangeFromUrlString: jest.fn().mockReturnValue(timeRange) | ||
| }) | ||
| ] | ||
| }); | ||
|  | ||
| cold('-a|', { | ||
| a: () => spectator.service.setTimeRangePreferenceForPage('foo', timeRange) | ||
| }).subscribe(update => update()); | ||
|  | ||
| const recordedTimeRanges$ = spectator.service | ||
| .getTimeRangePreferenceForPage('foo') | ||
| .pipe(map(timeRangeResolver => timeRangeResolver())); | ||
|  | ||
| expectObservable(recordedTimeRanges$).toBe('da', { | ||
| d: defaultPageTimeRange, | ||
| a: timeRange | ||
| }); | ||
| }); | ||
| }); | ||
|  | ||
| test('Setting relative time range emits corresponding time range from preferences', () => { | ||
| runFakeRxjs(({ expectObservable, cold }) => { | ||
| const timeRange = new RelativeTimeRange(new TimeDuration(1, TimeUnit.Hour)); | ||
| const spectator = serviceFactory({ | ||
| providers: [ | ||
| mockProvider(TimeRangeService, { | ||
| timeRangeFromUrlString: jest.fn().mockReturnValue(timeRange) | ||
| }) | ||
| ] | ||
| }); | ||
|  | ||
| cold('-b|', { | ||
| b: () => spectator.service.setTimeRangePreferenceForPage('bar', timeRange) | ||
| }).subscribe(update => update()); | ||
|  | ||
| const recordedTimeRanges$ = spectator.service | ||
| .getTimeRangePreferenceForPage('bar') | ||
| .pipe(map(timeRangeResolver => timeRangeResolver())); | ||
|  | ||
| expectObservable(recordedTimeRanges$).toBe('db', { | ||
| d: defaultPageTimeRange, | ||
| b: timeRange | ||
| }); | ||
| }); | ||
| }); | ||
| }); | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { Injectable } from '@angular/core'; | ||
| import { ActivatedRoute } from '@angular/router'; | ||
| import { isNil } from 'lodash-es'; | ||
| import { combineLatest, Observable } from 'rxjs'; | ||
| import { map, shareReplay, take } from 'rxjs/operators'; | ||
| import { ApplicationFeature } from '../constants/application-constants'; | ||
| import { FeatureStateResolver } from '../feature/state/feature-state.resolver'; | ||
| import { FeatureState } from '../feature/state/feature.state'; | ||
| import { NavigationService } from '../navigation/navigation.service'; | ||
| import { PreferenceService, StorageType } from '../preference/preference.service'; | ||
| import { RelativeTimeRange } from './relative-time-range'; | ||
| import { TimeDuration } from './time-duration'; | ||
| import { TimeRange } from './time-range'; | ||
| import { TimeRangeService } from './time-range.service'; | ||
| import { TimeUnit } from './time-unit.type'; | ||
|  | ||
| @Injectable({ providedIn: 'root' }) | ||
| export class PageTimeRangePreferenceService { | ||
| private static readonly STORAGE_TYPE: StorageType = StorageType.Local; | ||
| private static readonly TIME_RANGE_PREFERENCE_KEY: string = 'page-time-range'; | ||
|  | ||
| private readonly pageTimeRangeStringDictionary$: Observable<PageTimeRangeStringDictionary>; | ||
|  | ||
| public constructor( | ||
| private readonly preferenceService: PreferenceService, | ||
| private readonly timeRangeService: TimeRangeService, | ||
| private readonly navigationService: NavigationService, | ||
| private readonly featureStateResolver: FeatureStateResolver | ||
| ) { | ||
| this.pageTimeRangeStringDictionary$ = this.buildPageTimeRangeObservable(); | ||
| } | ||
|  | ||
| public getTimeRangePreferenceForPage(rootLevelPath: string): Observable<TimeRangeResolver> { | ||
| return combineLatest([ | ||
| this.pageTimeRangeStringDictionary$, | ||
| this.featureStateResolver.getFeatureState(ApplicationFeature.PageTimeRange) | ||
| ]).pipe( | ||
| map(([pageTimeRangeStringDictionary, featureState]) => { | ||
| if (featureState === FeatureState.Enabled) { | ||
| if (isNil(pageTimeRangeStringDictionary[rootLevelPath])) { | ||
| return () => this.getDefaultTimeRangeForCurrentRoute(); | ||
| } | ||
|  | ||
| return () => this.timeRangeService.timeRangeFromUrlString(pageTimeRangeStringDictionary[rootLevelPath]); | ||
| } | ||
|  | ||
| // When FF is disabled | ||
| return () => this.getGlobalDefaultTimeRange(); | ||
| }) | ||
| ); | ||
| } | ||
|  | ||
| public setTimeRangePreferenceForPage(rootLevelPath: string, value: TimeRange): void { | ||
| this.pageTimeRangeStringDictionary$.pipe(take(1)).subscribe(currentPageTimeRangeDictionary => { | ||
| this.setPreferenceServicePageTimeRange(currentPageTimeRangeDictionary, rootLevelPath, value); | ||
| }); | ||
| } | ||
|  | ||
| private setPreferenceServicePageTimeRange( | ||
| currentTimeRangeDictionary: PageTimeRangeStringDictionary, | ||
| rootLevelPath: string, | ||
| timeRange: TimeRange | ||
| ): void { | ||
| this.preferenceService.set( | ||
| PageTimeRangePreferenceService.TIME_RANGE_PREFERENCE_KEY, | ||
| { ...currentTimeRangeDictionary, [rootLevelPath]: timeRange.toUrlString() }, | ||
| PageTimeRangePreferenceService.STORAGE_TYPE | ||
| ); | ||
| } | ||
|  | ||
| private buildPageTimeRangeObservable(): Observable<PageTimeRangeStringDictionary> { | ||
| return this.preferenceService | ||
| .get<PageTimeRangeStringDictionary>( | ||
| PageTimeRangePreferenceService.TIME_RANGE_PREFERENCE_KEY, | ||
| {}, | ||
| PageTimeRangePreferenceService.STORAGE_TYPE | ||
| ) | ||
| .pipe(shareReplay(1)); | ||
| } | ||
|  | ||
| public getDefaultTimeRangeForCurrentRoute(): TimeRange { | ||
| const currentRoute: ActivatedRoute = this.navigationService.getCurrentActivatedRoute(); | ||
| // Right side for when FF is enabled but 'defaultTimeRange' is not set on AR data | ||
|  | ||
| return currentRoute.snapshot.data?.defaultTimeRange ?? this.getGlobalDefaultTimeRange(); | ||
| } | ||
|  | ||
| public getGlobalDefaultTimeRange(): TimeRange { | ||
| return new RelativeTimeRange(new TimeDuration(1, TimeUnit.Hour)); | ||
| } | ||
| } | ||
|  | ||
| interface PageTimeRangeStringDictionary { | ||
| [path: string]: string; | ||
| } | ||
| export type TimeRangeResolver = () => TimeRange; | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -1,5 +1,13 @@ | ||
| import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; | ||
| import { GLOBAL_HEADER_HEIGHT, NavigationService } from '@hypertrace/common'; | ||
| import { | ||
| ApplicationFeature, | ||
| FeatureState, | ||
| FeatureStateResolver, | ||
| GLOBAL_HEADER_HEIGHT, | ||
| NavigationService | ||
| } from '@hypertrace/common'; | ||
| import { Observable } from 'rxjs'; | ||
| import { map } from 'rxjs/operators'; | ||
|  | ||
| @Component({ | ||
| selector: 'ht-application-header', | ||
|  | @@ -15,9 +23,11 @@ import { GLOBAL_HEADER_HEIGHT, NavigationService } from '@hypertrace/common'; | |
| <div class="left-side-content"> | ||
| <ng-content select="[left]"></ng-content> | ||
| </div> | ||
| <div class="time-range" *ngIf="this.showTimeRange"> | ||
| <ht-time-range></ht-time-range> | ||
| </div> | ||
| <ng-container *ngIf="this.showTimeRange"> | ||
| <div class="time-range" *ngIf="this.pageLevelTimeRangeDisabled$ | async"> | ||
| <ht-time-range></ht-time-range> | ||
| </div> | ||
| </ng-container> | ||
| </div> | ||
| <div class="right-side-content"> | ||
| <ng-content></ng-content> | ||
|  | @@ -30,10 +40,17 @@ export class ApplicationHeaderComponent { | |
| @Input() | ||
| public showTimeRange: boolean = true; | ||
|  | ||
| public pageLevelTimeRangeDisabled$: Observable<boolean>; | ||
|  | ||
| public constructor( | ||
| @Inject(GLOBAL_HEADER_HEIGHT) public readonly height: string, | ||
| private readonly navigationService: NavigationService | ||
| ) {} | ||
| private readonly navigationService: NavigationService, | ||
| private readonly featureStateResolver: FeatureStateResolver | ||
| ) { | ||
| this.pageLevelTimeRangeDisabled$ = this.featureStateResolver | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inverted FF logic here so that two time ranges are not shown on screen at once. Time range is hidden here when FF is Enabled and shown when disabled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should move the invertFF logic to ht. @aaron-steinfeld | ||
| .getFeatureState(ApplicationFeature.PageTimeRange) | ||
| .pipe(map(featureState => featureState === FeatureState.Disabled)); | ||
| } | ||
|  | ||
| public onLogoClick(): void { | ||
| this.navigationService.navigateWithinApp(['']); // Empty route so we go to default screen | ||
|  | ||
Uh oh!
There was an error while loading. Please reload this page.