-
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.