From a58d5ba63d30ae98faaef551014144ad0f32a5c3 Mon Sep 17 00:00:00 2001 From: Jay Bhatt Date: Tue, 15 Mar 2022 20:15:13 +0530 Subject: [PATCH 1/5] chore: added filter chip service to exports --- projects/components/src/public-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts index b1811f15a..6c5312c3a 100644 --- a/projects/components/src/public-api.ts +++ b/projects/components/src/public-api.ts @@ -356,3 +356,4 @@ export { TooltipDirective } from './tooltip/tooltip.directive'; // Filter Url Service export * from './filtering/filter/filter-url.service'; +export * from './filtering/filter-bar/filter-chip/filter-chip.service'; From 9b3714ece3a867f14972e267e32376cd6b435d4f Mon Sep 17 00:00:00 2001 From: Jay Bhatt Date: Tue, 15 Mar 2022 20:15:55 +0530 Subject: [PATCH 2/5] fix(parse_url_filter): updated logic to first try exact match before fallback to current logic --- .../src/filtering/filter/parser/parsed-filter.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/projects/components/src/filtering/filter/parser/parsed-filter.ts b/projects/components/src/filtering/filter/parser/parsed-filter.ts index 5f854fff2..da0308726 100644 --- a/projects/components/src/filtering/filter/parser/parsed-filter.ts +++ b/projects/components/src/filtering/filter/parser/parsed-filter.ts @@ -52,10 +52,19 @@ export const tryParseStringForAttribute = ( nameFields: KeysWithType[] = ['displayName'] ): FilterAttributeExpression | undefined => { const [stringContainingFullAttribute] = text.trim().split(MAP_LHS_DELIMITER, 1); - // The string to the left of any delimeter must start with the attribute otherwise no match - const matchingNameField = nameFields.find(nameField => - stringContainingFullAttribute.toLowerCase().startsWith(attributeToTest[nameField].toLowerCase()) + + // Check if there is an exact match for the string left of the delimeter and attribute + let matchingNameField = nameFields.find( + nameField => stringContainingFullAttribute.toLowerCase() === attributeToTest[nameField].toLowerCase() ); + + // If there is no exact match, the string to the left of any delimeter must start with the attribute otherwise no match + if (!matchingNameField) { + matchingNameField = nameFields.find(nameField => + stringContainingFullAttribute.toLowerCase().startsWith(attributeToTest[nameField].toLowerCase()) + ); + } + if (!matchingNameField) { return undefined; } From 94aded645d7fd1b3984b438c3c6e90cf504c3f6a Mon Sep 17 00:00:00 2001 From: Jay Bhatt Date: Tue, 15 Mar 2022 20:16:53 +0530 Subject: [PATCH 3/5] feat(filter-bar): updated filter bar to be controlled component with 2 way binding for filters --- .../filter-bar/filter-bar.component.ts | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/projects/components/src/filtering/filter-bar/filter-bar.component.ts b/projects/components/src/filtering/filter-bar/filter-bar.component.ts index 982b5160a..540e31058 100644 --- a/projects/components/src/filtering/filter-bar/filter-bar.component.ts +++ b/projects/components/src/filtering/filter-bar/filter-bar.component.ts @@ -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): 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,14 +143,15 @@ 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 { @@ -146,12 +159,17 @@ export class FilterBarComponent implements OnChanges, OnInit, OnDestroy { 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)); } private deleteFilter(filter: Filter): void { - this.onFiltersChanged(this.filterBarService.deleteFilter(this.internalFiltersSubject$.value, filter)); + this.onFiltersChanged(this.filterBarService.deleteFilter(this.filters || [], filter)); } } From 984220dacefa9d03589f12c306967f8a2c0d0e08 Mon Sep 17 00:00:00 2001 From: Jay Bhatt Date: Tue, 15 Mar 2022 20:17:40 +0530 Subject: [PATCH 4/5] feat(explorer): added tab switch filter handling logic to explorer --- .../src/pages/explorer/explorer.component.ts | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/projects/observability/src/pages/explorer/explorer.component.ts b/projects/observability/src/pages/explorer/explorer.component.ts index 9f9f304a5..4634b9310 100644 --- a/projects/observability/src/pages/explorer/explorer.component.ts +++ b/projects/observability/src/pages/explorer/explorer.component.ts @@ -8,9 +8,9 @@ import { TimeDuration, TimeDurationService } from '@hypertrace/common'; -import { Filter, ToggleItem } from '@hypertrace/components'; +import { Filter, IncompleteFilter, ToggleItem, FilterChipService } 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)" >
@@ -145,13 +146,16 @@ export class ExplorerComponent { public visualizationExpanded$: Observable; public resultsExpanded$: Observable; - private readonly contextChangeSubject: Subject = new Subject(); + private readonly contextChangeSubject: BehaviorSubject = 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[]) { + return attrArray.map(({ name, displayName, units, type, onlySupportsAggregation, onlySupportsGrouping }) => { + const applicableType = toFilterAttributeType(type); + return { + name, + displayName, + units, + type: applicableType, + onlySupportsAggregation, + onlySupportsGrouping + }; + }); + } + public onContextUpdated(contextWrapper: ExplorerContextScope): void { this.attributes$ = this.metadataService.getFilterAttributes(contextWrapper.dashboardContext); + const listener = this.attributes$.subscribe(attributes => { + 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]) { + let newFilter = this.filterChipService.autocompleteFilters( + this.convertToFilterAttributes(attributes), + eachFilter.userString + ); + if (newFilter && newFilter.length !== 0) { + if (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 = { + [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' + } +}; From 926a34229099509cc44cbbee9466a51a4b7bafd2 Mon Sep 17 00:00:00 2001 From: Jay Bhatt Date: Tue, 15 Mar 2022 23:58:55 +0530 Subject: [PATCH 5/5] chore: removed linting errors from explorer component --- .../src/pages/explorer/explorer.component.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/projects/observability/src/pages/explorer/explorer.component.ts b/projects/observability/src/pages/explorer/explorer.component.ts index 4634b9310..eb1d7587c 100644 --- a/projects/observability/src/pages/explorer/explorer.component.ts +++ b/projects/observability/src/pages/explorer/explorer.component.ts @@ -8,7 +8,7 @@ import { TimeDuration, TimeDurationService } from '@hypertrace/common'; -import { Filter, IncompleteFilter, ToggleItem, FilterChipService } from '@hypertrace/components'; +import { Filter, FilterAttribute, FilterChipService, IncompleteFilter, ToggleItem } from '@hypertrace/components'; import { isEmpty, isNil } from 'lodash-es'; import { BehaviorSubject, concat, EMPTY, Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -198,16 +198,17 @@ export class ExplorerComponent { } } - private convertToFilterAttributes(attrArray: AttributeMetadata[]) { + private convertToFilterAttributes(attrArray: AttributeMetadata[]): FilterAttribute[] { return attrArray.map(({ name, displayName, units, type, onlySupportsAggregation, onlySupportsGrouping }) => { const applicableType = toFilterAttributeType(type); + return { - name, - displayName, - units, + name: name, + displayName: displayName, + units: units, type: applicableType, - onlySupportsAggregation, - onlySupportsGrouping + onlySupportsAggregation: onlySupportsAggregation, + onlySupportsGrouping: onlySupportsGrouping }; }); } @@ -217,16 +218,15 @@ export class ExplorerComponent { const listener = this.attributes$.subscribe(attributes => { 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 the given filter has a different name for the selected tab, update the filter value if (eachFilter.field in contextMapObject[lastTab]) { - let newFilter = this.filterChipService.autocompleteFilters( + const newFilter = this.filterChipService.autocompleteFilters( this.convertToFilterAttributes(attributes), eachFilter.userString ); - if (newFilter && newFilter.length !== 0) { - if (this.isValidFilter(newFilter[0])) { - return newFilter[0]; - } + + if (!isEmpty(newFilter) && this.isValidFilter(newFilter[0])) { + return newFilter[0]; } } @@ -239,7 +239,7 @@ export class ExplorerComponent { this.contextChangeSubject.next(contextWrapper.dashboardContext); } - private isValidFilter(incompleteFilter: IncompleteFilter): incompleteFilter is Filter { + private isValidFilter(incompleteFilter: IncompleteFilter): incompleteFilter is Filter { return incompleteFilter.operator !== undefined && incompleteFilter.value !== undefined; }