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)); } } 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; } diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts index cbdafa48e..9047748d8 100644 --- a/projects/components/src/public-api.ts +++ b/projects/components/src/public-api.ts @@ -360,3 +360,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'; diff --git a/projects/observability/src/pages/explorer/explorer.component.ts b/projects/observability/src/pages/explorer/explorer.component.ts index 9f9f304a5..eb1d7587c 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, 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)" >
@@ -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[]): 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 => { + 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( + 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 = { + [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' + } +};