-
Notifications
You must be signed in to change notification settings - Fork 12
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
Explorer - Fix Retaining of filters on switching between Trace and Span view #1486
Changes from all commits
a58d5ba
9b3714e
94aded6
984220d
1a508e0
926a342
b91643d
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 |
---|---|---|
|
@@ -12,7 +12,7 @@ import { | |
} from '@angular/core'; | ||
import { IconType } from '@hypertrace/assets-library'; | ||
import { TypedSimpleChanges } from '@hypertrace/common'; | ||
import { isEqual } from 'lodash-es'; | ||
import { isEmpty, isEqual } from 'lodash-es'; | ||
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; | ||
import { distinctUntilChanged, switchMap } from 'rxjs/operators'; | ||
import { IconSize } from '../../icon/icon-size'; | ||
|
@@ -108,6 +108,18 @@ export class FilterBarComponent implements OnChanges, OnInit, OnDestroy { | |
) {} | ||
|
||
public ngOnChanges(changes: TypedSimpleChanges<this>): void { | ||
if (changes.filters) { | ||
if (changes.attributes) { | ||
this.onFiltersChanged(this.filters || [], false, this.syncWithUrl); | ||
this.attributeSubject$.next(this.attributes || []); | ||
|
||
return; | ||
} | ||
|
||
// The local state should be in sync with the state passed by parent | ||
this.internalFiltersSubject$.next(changes.filters.currentValue || []); | ||
} | ||
|
||
if (changes.attributes) { | ||
this.attributeSubject$.next(this.attributes || []); | ||
this.syncWithUrl ? this.readFromUrlFilters() : this.onFiltersChanged(this.filters || [], false); | ||
|
@@ -131,27 +143,33 @@ export class FilterBarComponent implements OnChanges, OnInit, OnDestroy { | |
} | ||
|
||
private onFiltersChanged(filters: Filter[], emit: boolean = true, writeIfSyncEnabled: boolean = true): void { | ||
this.internalFiltersSubject$.next([...filters]); | ||
const newFilters: Filter[] = [...filters]; | ||
this.internalFiltersSubject$.next(newFilters); | ||
this.changeDetector.markForCheck(); | ||
|
||
if (writeIfSyncEnabled && this.syncWithUrl && !!this.attributes) { | ||
this.writeToUrlFilter(); | ||
this.writeToUrlFilter(newFilters); | ||
} | ||
|
||
emit && this.filtersChange.emit(this.internalFiltersSubject$.value); | ||
emit && this.filtersChange.emit(newFilters); | ||
} | ||
|
||
private readFromUrlFilters(): void { | ||
const filters = this.filterUrlService.getUrlFilters(this.attributes || []); | ||
this.onFiltersChanged(filters, true, false); | ||
} | ||
|
||
private writeToUrlFilter(): void { | ||
this.filterUrlService.setUrlFilters(this.internalFiltersSubject$.value); | ||
private writeToUrlFilter(filters: Filter[]): void { | ||
this.filterUrlService.setUrlFilters(filters); | ||
} | ||
|
||
public onInputApply(filter: Filter): void { | ||
this.onFiltersChanged(this.filterBarService.addFilter(this.internalFiltersSubject$.value, filter)); | ||
this.onFiltersChanged( | ||
this.filterBarService.addFilter( | ||
isEmpty(this.internalFiltersSubject$.value) ? [] : this.internalFiltersSubject$.value, | ||
filter | ||
) | ||
); | ||
this.resetFocus(); | ||
} | ||
|
||
|
@@ -175,10 +193,10 @@ export class FilterBarComponent implements OnChanges, OnInit, OnDestroy { | |
} | ||
|
||
private updateFilter(oldFilter: Filter, newFilter: Filter): void { | ||
this.onFiltersChanged(this.filterBarService.updateFilter(this.internalFiltersSubject$.value, oldFilter, newFilter)); | ||
this.onFiltersChanged(this.filterBarService.updateFilter(this.filters || [], oldFilter, newFilter)); | ||
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. When do we use the subject's value and when do we use this.filters? We seem to use different ways in different places which makes it very confusion to understand as a reader. |
||
} | ||
|
||
private deleteFilter(filter: Filter): void { | ||
this.onFiltersChanged(this.filterBarService.deleteFilter(this.internalFiltersSubject$.value, filter)); | ||
this.onFiltersChanged(this.filterBarService.deleteFilter(this.filters || [], filter)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,9 +8,9 @@ import { | |
TimeDuration, | ||
TimeDurationService | ||
} from '@hypertrace/common'; | ||
import { Filter, ToggleItem } from '@hypertrace/components'; | ||
import { Filter, FilterAttribute, FilterChipService, IncompleteFilter, ToggleItem } from '@hypertrace/components'; | ||
import { isEmpty, isNil } from 'lodash-es'; | ||
import { concat, EMPTY, Observable, Subject } from 'rxjs'; | ||
import { BehaviorSubject, concat, EMPTY, Observable } from 'rxjs'; | ||
import { map, take } from 'rxjs/operators'; | ||
import { CartesianSeriesVisualizationType } from '../../shared/components/cartesian/chart'; | ||
import { | ||
|
@@ -20,7 +20,7 @@ import { | |
} from '../../shared/components/explore-query-editor/explore-visualization-builder'; | ||
import { IntervalValue } from '../../shared/components/interval-select/interval-select.component'; | ||
import { AttributeExpression } from '../../shared/graphql/model/attribute/attribute-expression'; | ||
import { AttributeMetadata } from '../../shared/graphql/model/metadata/attribute-metadata'; | ||
import { AttributeMetadata, toFilterAttributeType } from '../../shared/graphql/model/metadata/attribute-metadata'; | ||
import { MetricAggregationType } from '../../shared/graphql/model/metrics/metric-aggregation'; | ||
import { GraphQlGroupBy } from '../../shared/graphql/model/schema/groupby/graphql-group-by'; | ||
import { ObservabilityTraceType } from '../../shared/graphql/model/schema/observability-traces'; | ||
|
@@ -52,6 +52,7 @@ import { | |
class="explorer-filter-bar" | ||
[attributes]="this.attributes$ | async" | ||
[syncWithUrl]="true" | ||
[filters]="this.filters" | ||
(filtersChange)="this.onFiltersUpdated($event)" | ||
></ht-filter-bar> | ||
<div class="explorer-content"> | ||
|
@@ -145,13 +146,16 @@ export class ExplorerComponent { | |
public visualizationExpanded$: Observable<boolean>; | ||
public resultsExpanded$: Observable<boolean>; | ||
|
||
private readonly contextChangeSubject: Subject<ExplorerGeneratedDashboardContext> = new Subject(); | ||
private readonly contextChangeSubject: BehaviorSubject<ExplorerGeneratedDashboardContext> = new BehaviorSubject( | ||
ObservabilityTraceType.Api as ExplorerGeneratedDashboardContext | ||
); | ||
|
||
public constructor( | ||
private readonly metadataService: MetadataService, | ||
private readonly navigationService: NavigationService, | ||
private readonly timeDurationService: TimeDurationService, | ||
private readonly preferenceService: PreferenceService, | ||
private readonly filterChipService: FilterChipService, | ||
@Inject(EXPLORER_DASHBOARD_BUILDER_FACTORY) explorerDashboardBuilderFactory: ExplorerDashboardBuilderFactory, | ||
activatedRoute: ActivatedRoute | ||
) { | ||
|
@@ -194,11 +198,51 @@ export class ExplorerComponent { | |
} | ||
} | ||
|
||
private convertToFilterAttributes(attrArray: AttributeMetadata[]): FilterAttribute[] { | ||
return attrArray.map(({ name, displayName, units, type, onlySupportsAggregation, onlySupportsGrouping }) => { | ||
const applicableType = toFilterAttributeType(type); | ||
|
||
return { | ||
name: name, | ||
displayName: displayName, | ||
units: units, | ||
type: applicableType, | ||
onlySupportsAggregation: onlySupportsAggregation, | ||
onlySupportsGrouping: onlySupportsGrouping | ||
}; | ||
}); | ||
} | ||
|
||
public onContextUpdated(contextWrapper: ExplorerContextScope): void { | ||
this.attributes$ = this.metadataService.getFilterAttributes(contextWrapper.dashboardContext); | ||
const listener = this.attributes$.subscribe(attributes => { | ||
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. Will try to move this block outside of this method in an independent structure. 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 shouldn't use a subscription here. Make the filters an observable, and subscribe via async pipe in the template. |
||
const lastTab = this.contextChangeSubject.getValue(); | ||
const newFilters = this.filters.map(eachFilter => { | ||
// If the given filter has a different name for the selected tab, update the filter value | ||
if (eachFilter.field in contextMapObject[lastTab]) { | ||
const newFilter = this.filterChipService.autocompleteFilters( | ||
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 be able to do this without the chip service, that's an impl detail from the filter bar. Can we use the filter builder service (I hate how complex we made this!) instead just read the details out of the old one and write them into the new one. May need to lookup the new attribute, too - but we shouldn't have to create it, it's already one of the ones in the array coming in. |
||
this.convertToFilterAttributes(attributes), | ||
eachFilter.userString | ||
); | ||
|
||
if (!isEmpty(newFilter) && this.isValidFilter(newFilter[0])) { | ||
return newFilter[0]; | ||
} | ||
} | ||
|
||
return eachFilter; | ||
}); | ||
|
||
this.filters = newFilters; | ||
}); | ||
listener.unsubscribe(); | ||
this.contextChangeSubject.next(contextWrapper.dashboardContext); | ||
} | ||
|
||
private isValidFilter(incompleteFilter: IncompleteFilter): incompleteFilter is Filter { | ||
return incompleteFilter.operator !== undefined && incompleteFilter.value !== undefined; | ||
} | ||
|
||
public onVisualizationExpandedChange(expanded: boolean): void { | ||
this.preferenceService.set(ExplorerComponent.VISUALIZATION_EXPANDED_PREFERENCE, expanded); | ||
} | ||
|
@@ -327,6 +371,12 @@ interface ExplorerContextScope { | |
scopeQueryParam: ScopeQueryParam; | ||
} | ||
|
||
type contextMap = { | ||
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. PascalCase for type names (also, we generally use interfaces if constructing a brand new type that's not projected from another. Naming here is also an issue. I think context is an overloaded term, and even knowing the goal of this PR it took me reading the object using this to understand what this was. Maybe something like type AttributeTranslationDictionary = { [key: string]: string; }
const spanToApiTraceAttributeTranslationDictionary = {...};
const apiTraceToSpanAttributeTranslationDictionary = {...}; |
||
[key in ExplorerGeneratedDashboardContext]: { | ||
[key: string]: string; | ||
}; | ||
}; | ||
|
||
export const enum ScopeQueryParam { | ||
EndpointTraces = 'endpoint-traces', | ||
Spans = 'spans' | ||
|
@@ -339,3 +389,18 @@ const enum ExplorerQueryParam { | |
GroupLimit = 'limit', | ||
Series = 'series' | ||
} | ||
|
||
const contextMapObject: contextMap = { | ||
API_TRACE: { | ||
protocol: 'protocolName', | ||
requestMethod: 'spanRequestMethod', | ||
requestUrl: 'spanRequestUrl', | ||
tags: 'spanTags' | ||
}, | ||
SPAN: { | ||
protocolName: 'protocol', | ||
spanRequestMethod: 'requestMethod', | ||
spanRequestUrl: 'requestUrl', | ||
spanTags: 'tags' | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
value should never be undefined, so we're just checking if empty and if so assigning empty? did some logic change that forced this? Either this isn't required or we've broken the assigned type.