diff --git a/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.ts b/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.ts index 66ae2dd20..eea4123c4 100644 --- a/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.ts +++ b/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.ts @@ -13,6 +13,7 @@ import { tryParseStringForAttribute } from '../../filter/parser/parsed-filter'; import { AbstractFilterParser } from '../../filter/parser/types/abstract-filter-parser'; +import { FilterValue } from './../../filter/filter'; @Injectable({ providedIn: 'root' @@ -61,7 +62,7 @@ export class FilterChipService { private buildIncompleteFiltersForAttribute( text: string, - filterBuilder: AbstractFilterBuilder, + filterBuilder: AbstractFilterBuilder, attributeExpression: FilterAttributeExpression ): IncompleteFilter[] { const topLevelOperatorFilters = filterBuilder.supportedTopLevelOperators().map(operator => ({ @@ -95,8 +96,8 @@ export class FilterChipService { } private buildIncompleteFilterForAttributeAndOperator( - filterBuilder: AbstractFilterBuilder, - filterParser: AbstractFilterParser, + filterBuilder: AbstractFilterBuilder, + filterParser: AbstractFilterParser, splitFilter: SplitFilter, text: string ): IncompleteFilter { @@ -132,7 +133,7 @@ export class FilterChipService { } private buildIncompleteFilterForPartialAttributeMatch( - filterBuilder: AbstractFilterBuilder, + filterBuilder: AbstractFilterBuilder, attribute: FilterAttribute ): IncompleteFilter { return { diff --git a/projects/components/src/filtering/filter-button/filter-button.component.ts b/projects/components/src/filtering/filter-button/filter-button.component.ts index 8eb5f54e5..0c4876661 100644 --- a/projects/components/src/filtering/filter-button/filter-button.component.ts +++ b/projects/components/src/filtering/filter-button/filter-button.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Out import { IconType } from '@hypertrace/assets-library'; import { IconSize } from '../../icon/icon-size'; import { FilterBuilderLookupService } from '../filter/builder/filter-builder-lookup.service'; -import { Filter } from '../filter/filter'; +import { Filter, FilterValue } from '../filter/filter'; import { FilterAttribute } from '../filter/filter-attribute'; import { FilterUrlService } from '../filter/filter-url.service'; @@ -45,7 +45,7 @@ export class FilterButtonComponent implements OnChanges { public attribute?: FilterAttribute; @Input() - public value?: unknown; + public value?: FilterValue; @Output() public readonly popoverOpen: EventEmitter = new EventEmitter(); diff --git a/projects/components/src/filtering/filter-modal/in-filter-modal.component.ts b/projects/components/src/filtering/filter-modal/in-filter-modal.component.ts index 693ac2077..d1033f5db 100644 --- a/projects/components/src/filtering/filter-modal/in-filter-modal.component.ts +++ b/projects/components/src/filtering/filter-modal/in-filter-modal.component.ts @@ -3,7 +3,7 @@ import { sortUnknown } from '@hypertrace/common'; import { ButtonRole } from '../../button/button'; import { ModalRef, MODAL_DATA } from '../../modal/modal'; import { FilterBuilderLookupService } from '../filter/builder/filter-builder-lookup.service'; -import { IncompleteFilter } from '../filter/filter'; +import { FilterValue, IncompleteFilter } from '../filter/filter'; import { FilterAttribute } from '../filter/filter-attribute'; import { FilterOperator } from '../filter/filter-operators'; import { FilterUrlService } from '../filter/filter-url.service'; @@ -43,7 +43,7 @@ import { FilterUrlService } from '../filter/filter-url.service'; }) export class InFilterModalComponent { public isSupported: boolean = false; - public selected: Set = new Set(); + public selected: Set = new Set(); public constructor( private readonly modalRef: ModalRef, @@ -91,7 +91,7 @@ export class InFilterModalComponent { this.modalRef.close(); } - public onChecked(checked: boolean, value: unknown): void { + public onChecked(checked: boolean, value: FilterValue): void { checked ? this.selected.add(value) : this.selected.delete(value); } } diff --git a/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.ts b/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.ts index 3f73184f1..e9bd3f7b8 100644 --- a/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.ts +++ b/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { assertUnreachable } from '@hypertrace/common'; +import { FilterValue } from '../filter'; import { FilterAttributeType } from '../filter-attribute-type'; import { AbstractFilterBuilder } from './types/abstract-filter-builder'; import { BooleanFilterBuilder } from './types/boolean-filter-builder'; @@ -11,7 +12,7 @@ import { StringMapFilterBuilder } from './types/string-map-filter-builder'; providedIn: 'root' }) export class FilterBuilderLookupService { - public lookup(type: FilterAttributeType): AbstractFilterBuilder { + public lookup(type: FilterAttributeType): AbstractFilterBuilder { switch (type) { case FilterAttributeType.Boolean: return new BooleanFilterBuilder(); diff --git a/projects/components/src/filtering/filter/builder/types/abstract-filter-builder.ts b/projects/components/src/filtering/filter/builder/types/abstract-filter-builder.ts index b981692d4..1ca7c796a 100644 --- a/projects/components/src/filtering/filter/builder/types/abstract-filter-builder.ts +++ b/projects/components/src/filtering/filter/builder/types/abstract-filter-builder.ts @@ -1,13 +1,13 @@ import { collapseWhitespace } from '@hypertrace/common'; import { isEmpty } from 'lodash-es'; -import { Filter } from '../../filter'; +import { Filter, FilterValue, IncompleteFilter } from '../../filter'; import { FilterAttribute } from '../../filter-attribute'; import { FilterAttributeType } from '../../filter-attribute-type'; import { MAP_LHS_DELIMITER } from '../../filter-delimiters'; import { FilterOperator, toUrlFilterOperator } from '../../filter-operators'; import { FilterAttributeExpression } from '../../parser/parsed-filter'; -export abstract class AbstractFilterBuilder { +export abstract class AbstractFilterBuilder { public abstract supportedAttributeType(): FilterAttributeType; public abstract supportedSubpathOperators(): FilterOperator[]; @@ -47,6 +47,30 @@ export abstract class AbstractFilterBuilder { }; } + public buildPartialFilter( + attribute: FilterAttribute, + operator?: FilterOperator, + value?: TValue, + subpath?: string + ): IncompleteFilter { + if ( + operator !== undefined && + ((isEmpty(subpath) && !this.supportedTopLevelOperators().includes(operator)) || + (!isEmpty(subpath) && !this.supportedSubpathOperators().includes(operator))) + ) { + throw Error(`Operator '${operator}' not supported for filter attribute type '${attribute.type}'`); + } + + return { + metadata: attribute, + field: attribute.name, + subpath: subpath, + operator: operator, + value: value, + userString: this.buildUserFilterString(attribute, subpath, operator, value) + }; + } + public buildUserFilterString( attribute: FilterAttribute, subpath?: string, diff --git a/projects/components/src/filtering/filter/filter-url.service.ts b/projects/components/src/filtering/filter/filter-url.service.ts index 54373b267..2de20d590 100644 --- a/projects/components/src/filtering/filter/filter-url.service.ts +++ b/projects/components/src/filtering/filter/filter-url.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { NavigationService } from '@hypertrace/common'; +import { remove } from 'lodash-es'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { FilterBuilderLookupService } from './builder/filter-builder-lookup.service'; @@ -25,6 +26,21 @@ export class FilterUrlService { return this.navigationService.navigation$.pipe(map(() => this.getUrlFilters(attributes))); } + public getUrlFiltersForAttributes(attributes: FilterAttribute[]): (Filter | IncompleteFilter)[] { + const urlFilters = this.getUrlFilters(attributes); + + return attributes.map(attribute => { + const match = urlFilters.find(f => f.field === attribute.name); + if (match !== undefined) { + remove(urlFilters, f => f === match); + + return match; + } + + return this.filterBuilderLookupService.lookup(attribute.type).buildPartialFilter(attribute); + }); + } + public getUrlFilters(attributes: FilterAttribute[]): Filter[] { return this.navigationService .getAllValuesForQueryParameter(FilterUrlService.FILTER_QUERY_PARAM) diff --git a/projects/components/src/filtering/filter/filter.ts b/projects/components/src/filtering/filter/filter.ts index 1f9cafaa8..a5f384a23 100644 --- a/projects/components/src/filtering/filter/filter.ts +++ b/projects/components/src/filtering/filter/filter.ts @@ -1,23 +1,26 @@ +import { Dictionary } from '@hypertrace/common'; import { FilterAttribute } from './filter-attribute'; import { FilterOperator, incompatibleOperators } from './filter-operators'; -export interface Filter extends IncompleteFilter { +export interface Filter extends IncompleteFilter { operator: FilterOperator; value: TValue; urlString: string; } -export interface IncompleteFilter extends FieldFilter { +export interface IncompleteFilter extends FieldFilter { metadata: FilterAttribute; userString: string; } -export interface FieldFilter { +export interface FieldFilter { field: string; subpath?: string; operator?: FilterOperator; value?: TValue; } +export type FilterValue = string | number | boolean | Date | Dictionary | FilterValue[]; + export const areCompatibleFilters = (f1: Filter, f2: Filter) => f1.field !== f2.field || f1.subpath !== f2.subpath || !incompatibleOperators(f1.operator).includes(f2.operator); diff --git a/projects/components/src/filtering/filter/parser/filter-parser-lookup.service.ts b/projects/components/src/filtering/filter/parser/filter-parser-lookup.service.ts index b1b215261..39fa96e14 100644 --- a/projects/components/src/filtering/filter/parser/filter-parser-lookup.service.ts +++ b/projects/components/src/filtering/filter/parser/filter-parser-lookup.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { assertUnreachable } from '@hypertrace/common'; +import { FilterValue } from '../filter'; import { FilterAttributeType } from '../filter-attribute-type'; import { FilterOperator } from '../filter-operators'; import { AbstractFilterParser } from './types/abstract-filter-parser'; @@ -14,7 +15,7 @@ export class FilterParserLookupService { // TODO remove the separate parsers entirely. // There's next to no logic left in them, and they duplicate (incorrectly) supported operators, // Which should be based on attribute type (as defined in filter builders) - public lookup(operator: FilterOperator): AbstractFilterParser { + public lookup(operator: FilterOperator): AbstractFilterParser { switch (operator) { case FilterOperator.Equals: case FilterOperator.NotEquals: diff --git a/projects/components/src/table/table-api.ts b/projects/components/src/table/table-api.ts index 764a93624..2bc448e10 100644 --- a/projects/components/src/table/table-api.ts +++ b/projects/components/src/table/table-api.ts @@ -1,6 +1,6 @@ import { Dictionary } from '@hypertrace/common'; import { Observable } from 'rxjs'; -import { FieldFilter } from '../filtering/filter/filter'; +import { FieldFilter, FilterValue } from '../filtering/filter/filter'; import { FilterOperator } from '../filtering/filter/filter-operators'; import { TableCellAlignmentType } from './cells/types/table-cell-alignment-type'; @@ -60,7 +60,7 @@ export interface RowStateChange { export interface TableFilter extends FieldFilter { operator: FilterOperator; - value: unknown; + value: FilterValue; } export const enum TableSortDirection { diff --git a/projects/observability/src/shared/components/explore-query-editor/explore-visualization-builder.ts b/projects/observability/src/shared/components/explore-query-editor/explore-visualization-builder.ts index ccddaba83..38522a5e7 100644 --- a/projects/observability/src/shared/components/explore-query-editor/explore-visualization-builder.ts +++ b/projects/observability/src/shared/components/explore-query-editor/explore-visualization-builder.ts @@ -128,7 +128,7 @@ export class ExploreVisualizationBuilder implements OnDestroy { selections: state.series.map(series => series.specification), context: state.context, interval: this.resolveInterval(state.interval), - filters: state.filters && this.graphQlFilterBuilderService.buildGraphQlFilters(state.filters), + filters: state.filters && this.graphQlFilterBuilderService.buildGraphQlFieldFilters(state.filters), groupBy: state.groupBy, limit: state.resultLimit }); @@ -178,7 +178,7 @@ export class ExploreVisualizationBuilder implements OnDestroy { traceType: traceType, properties: specifications, limit: 100, - filters: filters && this.graphQlFilterBuilderService.buildGraphQlFilters(filters) + filters: filters && this.graphQlFilterBuilderService.buildGraphQlFieldFilters(filters) }; } @@ -190,7 +190,7 @@ export class ExploreVisualizationBuilder implements OnDestroy { requestType: SPANS_GQL_REQUEST, properties: specifications, limit: 100, - filters: filters && this.graphQlFilterBuilderService.buildGraphQlFilters(filters) + filters: filters && this.graphQlFilterBuilderService.buildGraphQlFieldFilters(filters) }; } diff --git a/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.ts b/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.ts index 97a88f121..1b08d84e7 100644 --- a/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.ts +++ b/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.ts @@ -110,7 +110,10 @@ export class NavigableDashboardComponent implements OnChanges { const rootDataSource = dashboard.getRootDataSource(); rootDataSource ?.clearFilters() - .addFilters(...this.implicitFilters, ...this.graphQlFilterBuilderService.buildGraphQlFilters(explicitFilters)); + .addFilters( + ...this.implicitFilters, + ...this.graphQlFilterBuilderService.buildGraphQlFieldFilters(explicitFilters) + ); dashboard.refresh(); } } diff --git a/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-options-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-options-data-source.model.ts index c4a13ca65..74771874b 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-options-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-options-data-source.model.ts @@ -1,4 +1,4 @@ -import { FilterOperator, TableControlOptionType, TableSelectControlOption } from '@hypertrace/components'; +import { FilterOperator, FilterValue, TableControlOptionType, TableSelectControlOption } from '@hypertrace/components'; import { Model } from '@hypertrace/hyperdash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -17,7 +17,7 @@ export class EntitiesAttributeOptionsDataSourceModel extends EntitiesAttributeDa metaValue: { field: this.specification.name, operator: FilterOperator.Equals, - value: value + value: value as FilterValue } })) ) diff --git a/projects/observability/src/shared/dashboard/data/graphql/table/table-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/table/table-data-source.model.ts index e0f421062..e4b98f8b5 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/table/table-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/table/table-data-source.model.ts @@ -47,6 +47,6 @@ export abstract class TableDataSourceModel extends GraphQlDataSourceModel; protected toGraphQlFilters(tableFilters: TableFilter[] = []): GraphQlFilter[] { - return this.graphQlFilterBuilderService.buildGraphQlFilters(tableFilters); + return this.graphQlFilterBuilderService.buildGraphQlFiltersFromTableFilters(tableFilters); } } diff --git a/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts b/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts index b10f70950..6ffa3e233 100644 --- a/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts +++ b/projects/observability/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts @@ -615,7 +615,7 @@ export class TableWidgetRendererComponent private mergeFilters(tableFilter: TableFilter): TableFilter[] { const existingSelectFiltersWithChangedRemoved = this.removeFilters(tableFilter.field); - return [...existingSelectFiltersWithChangedRemoved, tableFilter].filter(f => f.value !== undefined); // Remove filters that are unset + return [...existingSelectFiltersWithChangedRemoved, tableFilter]; } private removeFilters(field: string): TableFilter[] { diff --git a/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.test.ts b/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.test.ts index 213376efe..bbe24424f 100644 --- a/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.test.ts +++ b/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.test.ts @@ -32,7 +32,7 @@ describe('Graphql filter builder service', () => { const spectator = serviceFactory(); expect( - spectator.service.buildGraphQlFilters([ + spectator.service.buildGraphQlFieldFilters([ buildFilter(attribute2, FilterOperator.Equals, 'foo'), buildFilter(attribute2, FilterOperator.NotEquals, 'bar'), buildFilter(attribute1, FilterOperator.GreaterThan, 5), diff --git a/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.ts b/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.ts index cc5d0bf47..81ffdaced 100644 --- a/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.ts +++ b/projects/observability/src/shared/services/filter-builder/graphql-filter-builder.service.ts @@ -1,13 +1,37 @@ import { Injectable } from '@angular/core'; import { assertUnreachable } from '@hypertrace/common'; -import { FilterOperator, TableFilter } from '@hypertrace/components'; +import { FieldFilter, FilterOperator, FilterValue, TableFilter } from '@hypertrace/components'; import { GraphQlArgumentValue } from '@hypertrace/graphql-client'; import { GraphQlFieldFilter } from '../../graphql/model/schema/filter/field/graphql-field-filter'; import { GraphQlFilter, GraphQlOperatorType } from '../../graphql/model/schema/filter/graphql-filter'; @Injectable({ providedIn: 'root' }) export class GraphQlFilterBuilderService { - public buildGraphQlFilters(filters: TableFilter[]): GraphQlFilter[] { + /** + * This is a temporary method to convert the GraphQL Field filters to its UI counterpart Filter Field Object. + */ + public buildFiltersFromGraphQlFieldFilters(filters: GraphQlFieldFilter[]): FieldFilter[] { + return filters.map(filter => ({ + field: typeof filter.keyOrExpression === 'string' ? filter.keyOrExpression : filter.keyOrExpression.key, + subpath: typeof filter.keyOrExpression === 'string' ? undefined : filter.keyOrExpression.subpath, + operator: toFilterOperator(filter.operator), + value: filter.value as FilterValue, + urlString: '' + })); + } + + public buildGraphQlFieldFilters(filters: FieldFilter[]): GraphQlFieldFilter[] { + return filters.map( + filter => + new GraphQlFieldFilter( + { key: filter.field, subpath: filter.subpath }, + toGraphQlOperator(filter.operator!), // Todo : Very weird + filter.value as GraphQlArgumentValue + ) + ); + } + + public buildGraphQlFiltersFromTableFilters(filters: TableFilter[]): GraphQlFilter[] { return filters.map( filter => new GraphQlFieldFilter( @@ -43,3 +67,39 @@ export const toGraphQlOperator = (operator: FilterOperator): GraphQlOperatorType return assertUnreachable(operator); } }; + +export const toFilterOperator = (operator: GraphQlOperatorType): FilterOperator => { + switch (operator) { + case GraphQlOperatorType.Equals: + return FilterOperator.Equals; + + case GraphQlOperatorType.NotEquals: + return FilterOperator.NotEquals; + + case GraphQlOperatorType.LessThan: + return FilterOperator.LessThan; + + case GraphQlOperatorType.LessThanOrEqualTo: + return FilterOperator.LessThanOrEqualTo; + + case GraphQlOperatorType.GreaterThan: + return FilterOperator.GreaterThan; + + case GraphQlOperatorType.GreaterThanOrEqualTo: + return FilterOperator.GreaterThanOrEqualTo; + + case GraphQlOperatorType.Like: + return FilterOperator.Like; + + case GraphQlOperatorType.In: + return FilterOperator.In; + + case GraphQlOperatorType.NotIn: + throw new Error('NotIn operator is not supported'); + + case GraphQlOperatorType.ContainsKey: + return FilterOperator.ContainsKey; + default: + return assertUnreachable(operator); + } +};