diff --git a/projects/common/src/utilities/types/types.ts b/projects/common/src/utilities/types/types.ts index 74a8cb7b6..de105c912 100644 --- a/projects/common/src/utilities/types/types.ts +++ b/projects/common/src/utilities/types/types.ts @@ -7,6 +7,8 @@ export type DistributiveOmit> = T extends any ? Omit = Omit & Partial>; export type RequireBy = T & Required>; +export type KeysWithType = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T]; + export interface Dictionary { [key: string]: T; } diff --git a/projects/components/src/filtering/filter-bar/filter-bar.service.test.ts b/projects/components/src/filtering/filter-bar/filter-bar.service.test.ts index 6348afc89..e3d07c9dd 100644 --- a/projects/components/src/filtering/filter-bar/filter-bar.service.test.ts +++ b/projects/components/src/filtering/filter-bar/filter-bar.service.test.ts @@ -35,8 +35,9 @@ describe('Filter Bar service', () => { ), new StringMapFilterBuilder().buildFilter( getTestFilterAttribute(FilterAttributeType.StringMap), - FilterOperator.ContainsKeyValue, - ['myKey', 'myValue'] + FilterOperator.Equals, + 'myValue', + 'myKey' ) ]; @@ -184,18 +185,33 @@ describe('Filter Bar service', () => { expect(testFilters).toEqual([stringFilter, inNumberFilter]); /* - * Add a StringMap CONTAINS_KEY_VALUE that should not replace any existing filters + * Add a StringMap EQUALS should not replace any existing filters */ - const ckvStringMapFilter = new StringMapFilterBuilder().buildFilter( + const firstStringMapFilter = new StringMapFilterBuilder().buildFilter( getTestFilterAttribute(FilterAttributeType.StringMap), - FilterOperator.ContainsKeyValue, - ['myKey', 'myValue'] + FilterOperator.Equals, + 'myValue', + 'myKey' ); - testFilters = spectator.service.addFilter(testFilters, ckvStringMapFilter); + testFilters = spectator.service.addFilter(testFilters, firstStringMapFilter); + + expect(testFilters).toEqual([stringFilter, inNumberFilter, firstStringMapFilter]); + /* + * Add a second StringMap EQUALS filters with a different key should not replace any existing filters + */ - expect(testFilters).toEqual([stringFilter, inNumberFilter, ckvStringMapFilter]); + const secondStringMapFilter = new StringMapFilterBuilder().buildFilter( + getTestFilterAttribute(FilterAttributeType.StringMap), + FilterOperator.Equals, + 'myValue', + 'mySecondKey' + ); + + testFilters = spectator.service.addFilter(testFilters, secondStringMapFilter); + + expect(testFilters).toEqual([stringFilter, inNumberFilter, firstStringMapFilter, secondStringMapFilter]); /* * Add a StringMap CONTAINS_KEY that should not replace any existing filters @@ -209,7 +225,13 @@ describe('Filter Bar service', () => { testFilters = spectator.service.addFilter(testFilters, ckStringMapFilter); - expect(testFilters).toEqual([stringFilter, inNumberFilter, ckvStringMapFilter, ckStringMapFilter]); + expect(testFilters).toEqual([ + stringFilter, + inNumberFilter, + firstStringMapFilter, + secondStringMapFilter, + ckStringMapFilter + ]); }); test('correctly updates filters', () => { @@ -221,8 +243,9 @@ describe('Filter Bar service', () => { const testStringMapFilter = new StringMapFilterBuilder().buildFilter( getTestFilterAttribute(FilterAttributeType.StringMap), - FilterOperator.ContainsKeyValue, - ['myTestKey', 'myTestValue'] + FilterOperator.Equals, + 'myTestValue', + 'myTestKey' ); const testNumberFilter = new NumberFilterBuilder().buildFilter( diff --git a/projects/components/src/filtering/filter-bar/filter-bar.service.ts b/projects/components/src/filtering/filter-bar/filter-bar.service.ts index 1aabc74c4..e9a5a7e10 100644 --- a/projects/components/src/filtering/filter-bar/filter-bar.service.ts +++ b/projects/components/src/filtering/filter-bar/filter-bar.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; -import { areCompatibleFilters, areEqualFilters, Filter } from '../filter/filter'; +import { isEqual } from 'lodash-es'; +import { areCompatibleFilters, Filter } from '../filter/filter'; @Injectable({ providedIn: 'root' @@ -14,7 +15,7 @@ export class FilterBarService { public updateFilter(filters: Filter[], oldFilter: Filter, newFilter: Filter): Filter[] { const clonedFilters = [...filters]; - const index = filters.findIndex(f => areEqualFilters(f, oldFilter)); + const index = filters.findIndex(f => isEqual(f, oldFilter)); if (index < 0) { throw new Error(`Unable to update filter. Filter for '${oldFilter.field}' not found.`); @@ -22,10 +23,10 @@ export class FilterBarService { clonedFilters.splice(index, 1, newFilter); - return clonedFilters.filter(f => areEqualFilters(f, newFilter) || areCompatibleFilters(f, newFilter)); + return clonedFilters.filter(f => isEqual(f, newFilter) || areCompatibleFilters(f, newFilter)); } public deleteFilter(filters: Filter[], filter: Filter): Filter[] { - return filters.filter(f => !areEqualFilters(f, filter)); + return filters.filter(f => !isEqual(f, filter)); } } diff --git a/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.component.ts b/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.component.ts index 2937fdc3b..b5294f54d 100644 --- a/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.component.ts +++ b/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.component.ts @@ -120,8 +120,7 @@ export class FilterChipComponent implements OnInit, OnChanges { private mapToComboBoxOption(filter: IncompleteFilter): ComboBoxOption { return { text: filter.userString, - value: filter, - tooltip: `${filter.userString} (${filter.field})` + value: filter }; } } diff --git a/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.test.ts b/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.test.ts index b926e36da..899fdb321 100644 --- a/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.test.ts +++ b/projects/components/src/filtering/filter-bar/filter-chip/filter-chip.service.test.ts @@ -62,7 +62,6 @@ describe('Filter Chip service', () => { case FilterOperator.In: return new InFilterParser(); case FilterOperator.ContainsKey: - case FilterOperator.ContainsKeyValue: return new ContainsFilterParser(); default: assertUnreachable(operator); @@ -246,21 +245,79 @@ describe('Filter Chip service', () => { ]); }); - test('correctly autocompletes operator filters for string map attribute once space key is entered', () => { + test('correctly autocompletes operator filters for string map attribute', () => { const attribute = getTestFilterAttribute(FilterAttributeType.StringMap); - expect(spectator.service.autocompleteFilters(attributes, 'String Map Attribute.testKey ')).toEqual([ + // Contains key or subpath operators if no subpath specified but could be + expect(spectator.service.autocompleteFilters(attributes, 'String Map Attribute')).toEqual([ { metadata: attribute, field: attribute.name, operator: FilterOperator.ContainsKey, - userString: `${attribute.displayName} ${FilterOperator.ContainsKey} testKey` + userString: `${attribute.displayName} ${FilterOperator.ContainsKey}` }, { metadata: attribute, field: attribute.name, - operator: FilterOperator.ContainsKeyValue, - userString: `${attribute.displayName}.testKey ${FilterOperator.ContainsKeyValue}` + operator: FilterOperator.Equals, + userString: `${attribute.displayName}.example ${FilterOperator.Equals}` + }, + { + metadata: attribute, + field: attribute.name, + operator: FilterOperator.NotEquals, + userString: `${attribute.displayName}.example ${FilterOperator.NotEquals}` + }, + { + metadata: attribute, + field: attribute.name, + operator: FilterOperator.In, + userString: `${attribute.displayName}.example ${FilterOperator.In}` + }, + { + metadata: attribute, + field: attribute.name, + operator: FilterOperator.Like, + userString: `${attribute.displayName}.example ${FilterOperator.Like}` + } + ]); + + // Regular operators only once subpath included + expect(spectator.service.autocompleteFilters(attributes, 'String Map Attribute.testKey')).toEqual([ + expect.objectContaining({ + metadata: attribute, + field: attribute.name, + operator: FilterOperator.ContainsKey, + // This operator isn't actually eligible but filtering operators is done by the chip/combobox, so just make sure the string doesn't match + userString: expect.not.stringMatching(`${attribute.displayName}.testKey`) + }), + { + metadata: attribute, + field: attribute.name, + subpath: 'testKey', + operator: FilterOperator.Equals, + userString: `${attribute.displayName}.testKey ${FilterOperator.Equals}` + }, + { + metadata: attribute, + field: attribute.name, + subpath: 'testKey', + operator: FilterOperator.NotEquals, + userString: `${attribute.displayName}.testKey ${FilterOperator.NotEquals}` + }, + { + metadata: attribute, + field: attribute.name, + subpath: 'testKey', + operator: FilterOperator.In, + userString: `${attribute.displayName}.testKey ${FilterOperator.In}` + }, + { + metadata: attribute, + field: attribute.name, + subpath: 'testKey', + operator: FilterOperator.Like, + userString: `${attribute.displayName}.testKey ${FilterOperator.Like}` } ]); }); 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 5aa5416d3..66ae2dd20 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 @@ -1,12 +1,18 @@ import { Injectable } from '@angular/core'; +import { isEmpty } from 'lodash-es'; import { FilterBuilderLookupService } from '../../filter/builder/filter-builder-lookup.service'; import { AbstractFilterBuilder } from '../../filter/builder/types/abstract-filter-builder'; import { IncompleteFilter } from '../../filter/filter'; import { FilterAttribute } from '../../filter/filter-attribute'; import { FilterOperator } from '../../filter/filter-operators'; import { FilterParserLookupService } from '../../filter/parser/filter-parser-lookup.service'; -import { SplitFilter } from '../../filter/parser/parsed-filter'; -import { AbstractFilterParser, splitFilterStringByOperator } from '../../filter/parser/types/abstract-filter-parser'; +import { + FilterAttributeExpression, + SplitFilter, + splitFilterStringByOperator, + tryParseStringForAttribute +} from '../../filter/parser/parsed-filter'; +import { AbstractFilterParser } from '../../filter/parser/types/abstract-filter-parser'; @Injectable({ providedIn: 'root' @@ -20,118 +26,119 @@ export class FilterChipService { public autocompleteFilters(attributes: FilterAttribute[], text: string = ''): IncompleteFilter[] { return attributes .filter(attribute => this.filterBuilderLookupService.isBuildableType(attribute.type)) - .flatMap(attribute => this.toIncompleteFilters(attribute, text)) - .filter(incompleteFilter => this.filterMatchesUserText(text, incompleteFilter)); + .flatMap(attribute => this.buildIncompleteFilters(attribute, text)); } - private filterMatchesUserText(text: string, incompleteFilter: IncompleteFilter): boolean { - const isStringMatch = incompleteFilter.userString.toLowerCase().includes(text.trim().toLowerCase()); - - if (isStringMatch || incompleteFilter.operator === undefined) { - return isStringMatch; - } - - /* - * In most cases, the isStringMatch should be the only check that is needed, however this check fails for - * STRING_MAP types with an operator since the LHS includes a user entered key between the attribute name - * and the operator. We fix this by sending the full string through our parsing logic to see if we can pull - * the key value off the LHS and build a filter out of it. If so, its a valid match and we want to include - * it as an autocomplete option. - */ - - return ( - this.filterParserLookupService - .lookup(incompleteFilter.operator) - .parseFilterString(incompleteFilter.metadata, incompleteFilter.userString) !== undefined - ); - } - - private toIncompleteFilters(attribute: FilterAttribute, text: string): IncompleteFilter[] { + private buildIncompleteFilters(attribute: FilterAttribute, text: string): IncompleteFilter[] { const filterBuilder = this.filterBuilderLookupService.lookup(attribute.type); - - // Check for operator - - const splitFilter = splitFilterStringByOperator(filterBuilder.supportedOperators(), text, true); - - if (splitFilter === undefined) { - // Unable to find operator - return this.toIncompleteFiltersWithoutOperator(filterBuilder, attribute, text); + const splitFilter = splitFilterStringByOperator(attribute, filterBuilder.allSupportedOperators(), text); + // If we've got a split filter we've got both an attribute and operator + if (splitFilter) { + return [ + this.buildIncompleteFilterForAttributeAndOperator( + filterBuilder, + this.filterParserLookupService.lookup(splitFilter.operator), + splitFilter, + text + ) + ]; } - // Operator found + // Next, look to see if this string starts with the attribute. If it does, continue on to see which operators also match the string. + const attributeExpression = tryParseStringForAttribute(attribute, text); + if (attributeExpression) { + return this.buildIncompleteFiltersForAttribute(text, filterBuilder, attributeExpression); + } - const filterParser = this.filterParserLookupService.lookup(splitFilter.operator); + // We can't figure out the attribute. If the partial string it could later match, present this attribute + if (this.isPartialAttributeMatch(text, attribute)) { + return [this.buildIncompleteFilterForPartialAttributeMatch(filterBuilder, attribute)]; + } - return this.toIncompleteFiltersWithOperator(filterBuilder, filterParser, splitFilter, attribute, text); + // Not even a partial match, present no options + return []; } - private toIncompleteFiltersWithoutOperator( + private buildIncompleteFiltersForAttribute( + text: string, filterBuilder: AbstractFilterBuilder, - attribute: FilterAttribute, - text: string + attributeExpression: FilterAttributeExpression ): IncompleteFilter[] { - if (text.toLowerCase().includes(attribute.displayName.toLowerCase()) && text.endsWith(' ')) { - // Attribute found, but no operator or value so let's provide all operator options for autocomplete - - return filterBuilder.supportedOperators().map(operator => { - const filterParser = this.filterParserLookupService.lookup(operator); - - const splitFilter = splitFilterStringByOperator([operator], `${text} ${operator} `); - const value = splitFilter === undefined ? undefined : filterParser.parseValueString(attribute, splitFilter); - - return { - metadata: attribute, - field: attribute.name, - operator: operator, - userString: filterBuilder.buildUserFilterString(attribute, operator, value) - }; - }); - } - - // Nothing matching yet, so just provide the attribute for autocomplete - - return [ - { - metadata: attribute, - field: attribute.name, - userString: filterBuilder.buildUserFilterString(attribute) - } - ]; + const topLevelOperatorFilters = filterBuilder.supportedTopLevelOperators().map(operator => ({ + metadata: attributeExpression.attribute, + field: attributeExpression.attribute.name, + operator: operator, + userString: filterBuilder.buildUserStringWithMatchingWhitespace( + text, + { attribute: attributeExpression.attribute }, + operator + ) + })); + + // Subpath operators should add a subpath placeholder to the user string + const subpathOperatorFilters = filterBuilder.supportedSubpathOperators().map(operator => ({ + metadata: attributeExpression.attribute, + field: attributeExpression.attribute.name, + subpath: attributeExpression.subpath, + operator: operator, + userString: filterBuilder.buildUserStringWithMatchingWhitespace( + text, + { + attribute: attributeExpression.attribute, + subpath: isEmpty(attributeExpression.subpath) ? 'example' : attributeExpression.subpath + }, + operator + ) + })); + + return [...topLevelOperatorFilters, ...subpathOperatorFilters]; } - private toIncompleteFiltersWithOperator( + private buildIncompleteFilterForAttributeAndOperator( filterBuilder: AbstractFilterBuilder, filterParser: AbstractFilterParser, splitFilter: SplitFilter, - attribute: FilterAttribute, text: string - ): IncompleteFilter[] { + ): IncompleteFilter { // Check for complete filter - const parsedFilter = filterParser.parseFilterString(attribute, text); + const parsedFilter = filterParser.parseSplitFilter(splitFilter); if (parsedFilter !== undefined) { // Found complete filter - - return [filterBuilder.buildFilter(attribute, parsedFilter.operator, parsedFilter.value)]; + return { + ...filterBuilder.buildFilter( + splitFilter.attribute, + parsedFilter.operator, + parsedFilter.value, + parsedFilter.subpath + ), + userString: text // Use the actual text provided by user, so it matches their input + }; } - // Not a complete filter, but we know we have an operator. Let's check if we also have an attribute - - if (splitFilter.lhs.trim().toLowerCase() === attribute.displayName.toLowerCase()) { - // Attribute found, and we have the operator but we do not have a value (else it would have been complete) - return [ - { - metadata: attribute, - field: attribute.name, - operator: splitFilter.operator, - userString: filterBuilder.buildUserFilterString(attribute, splitFilter.operator) - } - ]; - } + // Not a complete filter, but we know we have attribute and operator - + return { + metadata: splitFilter.attribute, + field: splitFilter.attribute.name, + operator: splitFilter.operator, + userString: text // Use the actual text provided by user, so it matches their input + }; + } - // This attribute not found in text + private isPartialAttributeMatch(text: string, attribute: FilterAttribute): boolean { + return attribute.displayName.toLowerCase().includes(text.toLowerCase()); + } - return []; + private buildIncompleteFilterForPartialAttributeMatch( + filterBuilder: AbstractFilterBuilder, + attribute: FilterAttribute + ): IncompleteFilter { + return { + metadata: attribute, + field: attribute.name, + userString: filterBuilder.buildUserFilterString(attribute) + }; } } diff --git a/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.test.ts b/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.test.ts index 50a8051b8..579b8e929 100644 --- a/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.test.ts +++ b/projects/components/src/filtering/filter/builder/filter-builder-lookup.service.test.ts @@ -177,17 +177,15 @@ describe('Filter Builder Lookup service', () => { expect( spectator.service .lookup(FilterAttributeType.StringMap) - .buildFilter(getTestFilterAttribute(FilterAttributeType.StringMap), FilterOperator.ContainsKeyValue, [ - 'myKey', - 'myValue' - ]) + .buildFilter(getTestFilterAttribute(FilterAttributeType.StringMap), FilterOperator.Equals, 'myValue', 'myKey') ).toEqual({ metadata: getTestFilterAttribute(FilterAttributeType.StringMap), field: getTestFilterAttribute(FilterAttributeType.StringMap).name, - operator: FilterOperator.ContainsKeyValue, - value: ['myKey', 'myValue'], - userString: 'String Map Attribute.myKey CONTAINS_KEY_VALUE myValue', - urlString: 'stringMapAttribute_ckv_myKey%3AmyValue' + subpath: 'myKey', + operator: FilterOperator.Equals, + value: 'myValue', + userString: 'String Map Attribute.myKey = myValue', + urlString: 'stringMapAttribute.myKey_eq_myValue' }); }); }); 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 7a1572a44..b981692d4 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,41 +1,95 @@ import { collapseWhitespace } from '@hypertrace/common'; +import { isEmpty } from 'lodash-es'; import { Filter } 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 { public abstract supportedAttributeType(): FilterAttributeType; - public abstract supportedOperators(): FilterOperator[]; + + public abstract supportedSubpathOperators(): FilterOperator[]; + public abstract supportedTopLevelOperators(): FilterOperator[]; protected abstract buildValueString(value: TValue): string; + public allSupportedOperators(): FilterOperator[] { + return [...this.supportedTopLevelOperators(), ...this.supportedSubpathOperators()]; + } + public buildFiltersForSupportedOperators(attribute: FilterAttribute, value: TValue): Filter[] { - return this.supportedOperators().map(operator => this.buildFilter(attribute, operator, value)); + return this.supportedTopLevelOperators().map(operator => this.buildFilter(attribute, operator, value)); } - public buildFilter(attribute: FilterAttribute, operator: FilterOperator, value: TValue): Filter { - if (!this.supportedOperators().includes(operator)) { + public buildFilter( + attribute: FilterAttribute, + operator: FilterOperator, + value: TValue, + subpath?: string + ): Filter { + if ( + (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, operator, value), - urlString: this.buildUrlFilterString(attribute, operator, value) + userString: this.buildUserFilterString(attribute, subpath, operator, value), + urlString: this.buildUrlFilterString(attribute, subpath, operator, value) }; } - public buildUserFilterString(attribute: FilterAttribute, operator?: FilterOperator, value?: TValue): string { + public buildUserFilterString( + attribute: FilterAttribute, + subpath?: string, + operator?: FilterOperator, + value?: TValue + ): string { + const attributeString = this.buildAttributeExpressionString(attribute.displayName, subpath); + return collapseWhitespace( - `${attribute.displayName} ${operator ?? ''} ${value !== undefined ? this.buildValueString(value) : ''}` + `${attributeString} ${operator ?? ''} ${value !== undefined ? this.buildValueString(value) : ''}` ).trim(); } - protected buildUrlFilterString(attribute: FilterAttribute, operator: FilterOperator, value: TValue): string { - return encodeURIComponent(`${attribute.name}${toUrlFilterOperator(operator)}${this.buildValueString(value)}`); + public buildUserStringWithMatchingWhitespace( + userEnteredString: string, + attributeExpression: FilterAttributeExpression, + operator: FilterOperator + ): string { + const attributeString = this.buildAttributeExpressionString( + attributeExpression.attribute.displayName, + attributeExpression.subpath + ); + const userStringAfterAttributeExpression = userEnteredString.slice(attributeString.length); + const whitespace = + userStringAfterAttributeExpression.length === 0 + ? ' ' + : userStringAfterAttributeExpression.match(/(\s*)/)?.[0] ?? ''; + + return `${attributeString}${whitespace}${operator}`; + } + + protected buildUrlFilterString( + attribute: FilterAttribute, + subpath: string | undefined, + operator: FilterOperator, + value: TValue + ): string { + const attributeString = this.buildAttributeExpressionString(attribute.name, subpath); + + return encodeURIComponent(`${attributeString}${toUrlFilterOperator(operator)}${this.buildValueString(value)}`); + } + + private buildAttributeExpressionString(attributeString: string, subpath?: string): string { + return isEmpty(subpath) ? attributeString : `${attributeString}${MAP_LHS_DELIMITER}${subpath}`; } } diff --git a/projects/components/src/filtering/filter/builder/types/boolean-filter-builder.ts b/projects/components/src/filtering/filter/builder/types/boolean-filter-builder.ts index f0ec4d719..bf9015b67 100644 --- a/projects/components/src/filtering/filter/builder/types/boolean-filter-builder.ts +++ b/projects/components/src/filtering/filter/builder/types/boolean-filter-builder.ts @@ -7,10 +7,14 @@ export class BooleanFilterBuilder extends AbstractFilterBuilder { return FilterAttributeType.Boolean; } - public supportedOperators(): FilterOperator[] { + public supportedTopLevelOperators(): FilterOperator[] { return [FilterOperator.Equals, FilterOperator.NotEquals]; } + public supportedSubpathOperators(): FilterOperator[] { + return []; + } + protected buildValueString(value: boolean): string { return String(value); } diff --git a/projects/components/src/filtering/filter/builder/types/number-filter-builder.ts b/projects/components/src/filtering/filter/builder/types/number-filter-builder.ts index 6b2c24db4..b742d06a8 100644 --- a/projects/components/src/filtering/filter/builder/types/number-filter-builder.ts +++ b/projects/components/src/filtering/filter/builder/types/number-filter-builder.ts @@ -8,7 +8,7 @@ export class NumberFilterBuilder extends AbstractFilterBuilder { +export class StringMapFilterBuilder extends AbstractFilterBuilder { public supportedAttributeType(): FilterAttributeType { return FilterAttributeType.StringMap; } - public supportedOperators(): FilterOperator[] { - return [FilterOperator.ContainsKey, FilterOperator.ContainsKeyValue]; + public supportedTopLevelOperators(): FilterOperator[] { + return [FilterOperator.ContainsKey]; } - protected buildValueString(value: string | [string, string]): string { - return Array.isArray(value) ? value.join(MAP_RHS_DELIMITER) : value; + public supportedSubpathOperators(): FilterOperator[] { + return [FilterOperator.Equals, FilterOperator.NotEquals, FilterOperator.In, FilterOperator.Like]; } - public buildUserFilterString( - attribute: FilterAttribute, - operator?: FilterOperator, - value?: string | [string, string] - ): string { - const lhs = this.buildUserFilterStringLhs(attribute, operator, value); - const rhs = this.buildUserFilterStringRhs(operator, value); - - return collapseWhitespace(`${lhs} ${operator ?? ''} ${rhs}`).trim(); - } - - private buildUserFilterStringLhs( - attribute: FilterAttribute, - operator?: FilterOperator, - value?: string | [string, string] - ): string { - if (operator === FilterOperator.ContainsKey || operator === undefined || value === undefined) { - return attribute.displayName; - } - - const displayValue: string = Array.isArray(value) ? (value.length > 0 ? value[0] : '') : value; - - return `${attribute.displayName}${MAP_LHS_DELIMITER}${displayValue}`; - } - - private buildUserFilterStringRhs(operator?: FilterOperator, value?: string | string[]): string { - return operator === FilterOperator.ContainsKey - ? Array.isArray(value) - ? value[0] ?? '' - : value ?? '' - : Array.isArray(value) - ? value[1] ?? '' - : value ?? ''; + protected buildValueString(value: string): string { + return String(value); } } diff --git a/projects/components/src/filtering/filter/filter-delimiters.ts b/projects/components/src/filtering/filter/filter-delimiters.ts index 008a4b4fa..c26dac8c9 100644 --- a/projects/components/src/filtering/filter/filter-delimiters.ts +++ b/projects/components/src/filtering/filter/filter-delimiters.ts @@ -1,3 +1,2 @@ export const ARRAY_DELIMITER = ','; export const MAP_LHS_DELIMITER = '.'; -export const MAP_RHS_DELIMITER = ':'; diff --git a/projects/components/src/filtering/filter/filter-operators.ts b/projects/components/src/filtering/filter/filter-operators.ts index 69f6029a4..36c92ecb1 100644 --- a/projects/components/src/filtering/filter/filter-operators.ts +++ b/projects/components/src/filtering/filter/filter-operators.ts @@ -9,8 +9,7 @@ export const enum FilterOperator { GreaterThanOrEqualTo = '>=', Like = '~', In = 'IN', - ContainsKey = 'CONTAINS_KEY', - ContainsKeyValue = 'CONTAINS_KEY_VALUE' + ContainsKey = 'CONTAINS_KEY' } export const enum UrlFilterOperator { @@ -22,8 +21,7 @@ export const enum UrlFilterOperator { GreaterThanOrEqualTo = '_gte_', Like = '_lk_', In = '_in_', - ContainsKey = '_ck_', - ContainsKeyValue = '_ckv_' + ContainsKey = '_ck_' } export const toUrlFilterOperator = (operator: FilterOperator): UrlFilterOperator => { @@ -46,8 +44,6 @@ export const toUrlFilterOperator = (operator: FilterOperator): UrlFilterOperator return UrlFilterOperator.In; case FilterOperator.ContainsKey: return UrlFilterOperator.ContainsKey; - case FilterOperator.ContainsKeyValue: - return UrlFilterOperator.ContainsKeyValue; default: return assertUnreachable(operator); } @@ -73,8 +69,6 @@ export const fromUrlFilterOperator = (operator: UrlFilterOperator): FilterOperat return FilterOperator.In; case UrlFilterOperator.ContainsKey: return FilterOperator.ContainsKey; - case UrlFilterOperator.ContainsKeyValue: - return FilterOperator.ContainsKeyValue; default: return assertUnreachable(operator); } @@ -93,11 +87,9 @@ export const incompatibleOperators = (operator: FilterOperator): FilterOperator[ FilterOperator.GreaterThan, FilterOperator.GreaterThanOrEqualTo, FilterOperator.Like, - FilterOperator.ContainsKey, - FilterOperator.ContainsKeyValue + FilterOperator.ContainsKey ]; case FilterOperator.ContainsKey: - case FilterOperator.ContainsKeyValue: case FilterOperator.NotEquals: case FilterOperator.Like: return [FilterOperator.In, FilterOperator.Equals]; diff --git a/projects/components/src/filtering/filter/filter-url.service.test.ts b/projects/components/src/filtering/filter/filter-url.service.test.ts index cb7396c50..5de0a1b8e 100644 --- a/projects/components/src/filtering/filter/filter-url.service.test.ts +++ b/projects/components/src/filtering/filter/filter-url.service.test.ts @@ -39,8 +39,9 @@ describe('Filter URL service', () => { ), new StringMapFilterBuilder().buildFilter( getTestFilterAttribute(FilterAttributeType.StringMap), - FilterOperator.ContainsKeyValue, - ['myKey', 'myValue'] + FilterOperator.Equals, + 'myValue', + 'myKey' ) ]; @@ -50,7 +51,7 @@ describe('Filter URL service', () => { 'numberAttribute_neq_415', 'numberAttribute_lte_707', 'stringAttribute_eq_test', - 'stringMapAttribute_ckv_myKey%3AmyValue' + 'stringMapAttribute.myKey_eq_myValue' ] }; @@ -232,19 +233,41 @@ describe('Filter URL service', () => { }); /* - * Add a StringMap CONTAINS_KEY_VALUE that should not replace any existing filters + * Add a StringMap EQUALS that should not replace any existing filters */ spectator.service.addUrlFilter( attributes, new StringMapFilterBuilder().buildFilter( getTestFilterAttribute(FilterAttributeType.StringMap), - FilterOperator.ContainsKeyValue, - ['myKey', 'myValue'] + FilterOperator.Equals, + 'myValue', + 'myKey' ) ); expect(testQueryParamObject).toEqual({ - filter: ['stringAttribute_neq_test', 'numberAttribute_in_1984', 'stringMapAttribute_ckv_myKey%3AmyValue'] + filter: ['stringAttribute_neq_test', 'numberAttribute_in_1984', 'stringMapAttribute.myKey_eq_myValue'] + }); + + /* + * Add a second StringMap EQUALS that should not replace any existing filters + */ + spectator.service.addUrlFilter( + attributes, + new StringMapFilterBuilder().buildFilter( + getTestFilterAttribute(FilterAttributeType.StringMap), + FilterOperator.Equals, + 'myValue', + 'mySecondKey' + ) + ); + expect(testQueryParamObject).toEqual({ + filter: [ + 'stringAttribute_neq_test', + 'numberAttribute_in_1984', + 'stringMapAttribute.myKey_eq_myValue', + 'stringMapAttribute.mySecondKey_eq_myValue' + ] }); /* @@ -263,7 +286,8 @@ describe('Filter URL service', () => { filter: [ 'stringAttribute_neq_test', 'numberAttribute_in_1984', - 'stringMapAttribute_ckv_myKey%3AmyValue', + 'stringMapAttribute.myKey_eq_myValue', + 'stringMapAttribute.mySecondKey_eq_myValue', 'stringMapAttribute_ck_myTestKey' ] }); @@ -283,7 +307,7 @@ describe('Filter URL service', () => { spectator.service.removeUrlFilter(attributes, testFilter); expect(testQueryParamObject).toEqual({ - filter: ['stringAttribute_eq_test', 'stringMapAttribute_ckv_myKey%3AmyValue'] + filter: ['stringAttribute_eq_test', 'stringMapAttribute.myKey_eq_myValue'] }); }); @@ -300,7 +324,7 @@ describe('Filter URL service', () => { spectator.service.removeUrlFilter(attributes, testFilter); expect(testQueryParamObject).toEqual({ - filter: ['numberAttribute_lte_707', 'stringAttribute_eq_test', 'stringMapAttribute_ckv_myKey%3AmyValue'] + filter: ['numberAttribute_lte_707', 'stringAttribute_eq_test', 'stringMapAttribute.myKey_eq_myValue'] }); }); @@ -326,14 +350,15 @@ describe('Filter URL service', () => { 'numberAttribute_neq_217', 'numberAttribute_lte_707', 'stringAttribute_eq_test', - 'stringMapAttribute_ckv_myKey%3AmyValue' + + 'stringMapAttribute.myKey_eq_myValue' ] }); spectator.service.removeUrlFilter(attributes, test2Filter); expect(testQueryParamObject).toEqual({ - filter: ['numberAttribute_neq_217', 'stringAttribute_eq_test', 'stringMapAttribute_ckv_myKey%3AmyValue'] + filter: ['numberAttribute_neq_217', 'stringAttribute_eq_test', 'stringMapAttribute.myKey_eq_myValue'] }); }); }); diff --git a/projects/components/src/filtering/filter/filter-url.service.ts b/projects/components/src/filtering/filter/filter-url.service.ts index 261d5c396..54373b267 100644 --- a/projects/components/src/filtering/filter/filter-url.service.ts +++ b/projects/components/src/filtering/filter/filter-url.service.ts @@ -7,7 +7,7 @@ import { areCompatibleFilters, Filter, IncompleteFilter } from './filter'; import { FilterAttribute } from './filter-attribute'; import { fromUrlFilterOperator, toUrlFilterOperator } from './filter-operators'; import { FilterParserLookupService } from './parser/filter-parser-lookup.service'; -import { splitFilterStringByOperator } from './parser/types/abstract-filter-parser'; +import { splitFilterStringByOperator } from './parser/parsed-filter'; @Injectable({ providedIn: 'root' @@ -62,25 +62,29 @@ export class FilterUrlService { .filter(attribute => this.filterBuilderLookupService.isBuildableType(attribute.type)) .flatMap(attribute => { const filterBuilder = this.filterBuilderLookupService.lookup(attribute.type); - const supportedUrlOperators = filterBuilder.supportedOperators().map(toUrlFilterOperator); + const supportedUrlOperators = filterBuilder.allSupportedOperators().map(toUrlFilterOperator); - const splitUrlFilter = splitFilterStringByOperator(supportedUrlOperators, filterString, false); + const splitUrlFilter = splitFilterStringByOperator( + attribute, + supportedUrlOperators, + decodeURIComponent(filterString) + ); if (splitUrlFilter === undefined) { return undefined; } - const filterParser = this.filterParserLookupService.lookup(fromUrlFilterOperator(splitUrlFilter.operator)); + const convertedOperator = fromUrlFilterOperator(splitUrlFilter.operator); - const parsedFilter = filterParser.parseUrlFilterString(attribute, filterString); + const parsedFilter = this.filterParserLookupService.lookup(convertedOperator).parseSplitFilter({ + ...splitUrlFilter, + operator: convertedOperator + }); - if (parsedFilter === undefined) { - return undefined; - } - - return splitUrlFilter.lhs === parsedFilter.field - ? filterBuilder.buildFilter(attribute, parsedFilter.operator, parsedFilter.value) - : undefined; + return ( + parsedFilter && + filterBuilder.buildFilter(attribute, parsedFilter.operator, parsedFilter.value, parsedFilter.subpath) + ); }) .find(splitFilter => splitFilter !== undefined); } diff --git a/projects/components/src/filtering/filter/filter.ts b/projects/components/src/filtering/filter/filter.ts index 5e3ca5cd7..1f9cafaa8 100644 --- a/projects/components/src/filtering/filter/filter.ts +++ b/projects/components/src/filtering/filter/filter.ts @@ -14,16 +14,10 @@ export interface IncompleteFilter extends FieldFilter export interface FieldFilter { field: string; + subpath?: string; operator?: FilterOperator; value?: TValue; } -export const areEqualFilters = (f1: IncompleteFilter, f2: IncompleteFilter) => - (f1.field === f2.field && f1.operator === undefined) || - f2.operator === undefined || - (f1.operator === f2.operator && f1.value === undefined) || - f2.value === undefined || - f1.value === f2.value; - export const areCompatibleFilters = (f1: Filter, f2: Filter) => - f1.field !== f2.field || (f1.field === f2.field && !incompatibleOperators(f1.operator).includes(f2.operator)); + 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.test.ts b/projects/components/src/filtering/filter/parser/filter-parser-lookup.service.test.ts index bd836c75f..d2cf065e9 100644 --- a/projects/components/src/filtering/filter/parser/filter-parser-lookup.service.test.ts +++ b/projects/components/src/filtering/filter/parser/filter-parser-lookup.service.test.ts @@ -28,7 +28,6 @@ describe('Filter Parser Lookup service', () => { expect(spectator.service.lookup(FilterOperator.In)).toEqual(expect.any(InFilterParser)); expect(spectator.service.lookup(FilterOperator.ContainsKey)).toEqual(expect.any(ContainsFilterParser)); - expect(spectator.service.lookup(FilterOperator.ContainsKeyValue)).toEqual(expect.any(ContainsFilterParser)); }); test('correctly identify parsable operators for a type', () => { @@ -42,7 +41,7 @@ describe('Filter Parser Lookup service', () => { true ); expect(spectator.service.isParsableOperatorForType(FilterOperator.Equals, FilterAttributeType.StringMap)).toEqual( - false + true ); expect(spectator.service.isParsableOperatorForType(FilterOperator.Equals, FilterAttributeType.Timestamp)).toEqual( false @@ -59,7 +58,7 @@ describe('Filter Parser Lookup service', () => { ); expect( spectator.service.isParsableOperatorForType(FilterOperator.NotEquals, FilterAttributeType.StringMap) - ).toEqual(false); + ).toEqual(true); expect( spectator.service.isParsableOperatorForType(FilterOperator.NotEquals, FilterAttributeType.Timestamp) ).toEqual(false); @@ -73,9 +72,7 @@ describe('Filter Parser Lookup service', () => { expect(spectator.service.isParsableOperatorForType(FilterOperator.LessThan, FilterAttributeType.String)).toEqual( true ); - expect(spectator.service.isParsableOperatorForType(FilterOperator.LessThan, FilterAttributeType.StringMap)).toEqual( - false - ); + expect(spectator.service.isParsableOperatorForType(FilterOperator.LessThan, FilterAttributeType.Timestamp)).toEqual( false ); @@ -89,9 +86,7 @@ describe('Filter Parser Lookup service', () => { expect( spectator.service.isParsableOperatorForType(FilterOperator.LessThanOrEqualTo, FilterAttributeType.String) ).toEqual(true); - expect( - spectator.service.isParsableOperatorForType(FilterOperator.LessThanOrEqualTo, FilterAttributeType.StringMap) - ).toEqual(false); + expect( spectator.service.isParsableOperatorForType(FilterOperator.LessThanOrEqualTo, FilterAttributeType.Timestamp) ).toEqual(false); @@ -105,9 +100,7 @@ describe('Filter Parser Lookup service', () => { expect(spectator.service.isParsableOperatorForType(FilterOperator.GreaterThan, FilterAttributeType.String)).toEqual( true ); - expect( - spectator.service.isParsableOperatorForType(FilterOperator.GreaterThan, FilterAttributeType.StringMap) - ).toEqual(false); + expect( spectator.service.isParsableOperatorForType(FilterOperator.GreaterThan, FilterAttributeType.Timestamp) ).toEqual(false); @@ -121,9 +114,6 @@ describe('Filter Parser Lookup service', () => { expect( spectator.service.isParsableOperatorForType(FilterOperator.GreaterThanOrEqualTo, FilterAttributeType.String) ).toEqual(true); - expect( - spectator.service.isParsableOperatorForType(FilterOperator.GreaterThanOrEqualTo, FilterAttributeType.StringMap) - ).toEqual(false); expect( spectator.service.isParsableOperatorForType(FilterOperator.GreaterThanOrEqualTo, FilterAttributeType.Timestamp) ).toEqual(false); @@ -131,9 +121,7 @@ describe('Filter Parser Lookup service', () => { expect(spectator.service.isParsableOperatorForType(FilterOperator.In, FilterAttributeType.Boolean)).toEqual(false); expect(spectator.service.isParsableOperatorForType(FilterOperator.In, FilterAttributeType.Number)).toEqual(true); expect(spectator.service.isParsableOperatorForType(FilterOperator.In, FilterAttributeType.String)).toEqual(true); - expect(spectator.service.isParsableOperatorForType(FilterOperator.In, FilterAttributeType.StringMap)).toEqual( - false - ); + expect(spectator.service.isParsableOperatorForType(FilterOperator.In, FilterAttributeType.StringMap)).toEqual(true); expect(spectator.service.isParsableOperatorForType(FilterOperator.In, FilterAttributeType.Timestamp)).toEqual( false ); @@ -153,29 +141,15 @@ describe('Filter Parser Lookup service', () => { expect( spectator.service.isParsableOperatorForType(FilterOperator.ContainsKey, FilterAttributeType.Timestamp) ).toEqual(false); - - expect( - spectator.service.isParsableOperatorForType(FilterOperator.ContainsKeyValue, FilterAttributeType.Boolean) - ).toEqual(false); - expect( - spectator.service.isParsableOperatorForType(FilterOperator.ContainsKeyValue, FilterAttributeType.Number) - ).toEqual(false); - expect( - spectator.service.isParsableOperatorForType(FilterOperator.ContainsKeyValue, FilterAttributeType.String) - ).toEqual(false); - expect( - spectator.service.isParsableOperatorForType(FilterOperator.ContainsKeyValue, FilterAttributeType.StringMap) - ).toEqual(true); - expect( - spectator.service.isParsableOperatorForType(FilterOperator.ContainsKeyValue, FilterAttributeType.Timestamp) - ).toEqual(false); }); - test('can provide a parser to parse user filter strings', () => { + test('can provide a parser to parse split filters', () => { expect( - spectator.service - .lookup(FilterOperator.Equals) - .parseFilterString(getTestFilterAttribute(FilterAttributeType.Boolean), 'Boolean Attribute = false') + spectator.service.lookup(FilterOperator.Equals).parseSplitFilter({ + attribute: getTestFilterAttribute(FilterAttributeType.Boolean), + operator: FilterOperator.Equals, + rhs: 'false' + }) ).toEqual({ field: 'booleanAttribute', operator: FilterOperator.Equals, @@ -183,9 +157,11 @@ describe('Filter Parser Lookup service', () => { }); expect( - spectator.service - .lookup(FilterOperator.LessThanOrEqualTo) - .parseFilterString(getTestFilterAttribute(FilterAttributeType.Number), 'Number Attribute <= 217') + spectator.service.lookup(FilterOperator.LessThanOrEqualTo).parseSplitFilter({ + attribute: getTestFilterAttribute(FilterAttributeType.Number), + operator: FilterOperator.LessThanOrEqualTo, + rhs: '217' + }) ).toEqual({ field: 'numberAttribute', operator: FilterOperator.LessThanOrEqualTo, @@ -193,9 +169,11 @@ describe('Filter Parser Lookup service', () => { }); expect( - spectator.service - .lookup(FilterOperator.NotEquals) - .parseFilterString(getTestFilterAttribute(FilterAttributeType.String), 'String Attribute != myString') + spectator.service.lookup(FilterOperator.NotEquals).parseSplitFilter({ + attribute: getTestFilterAttribute(FilterAttributeType.String), + operator: FilterOperator.NotEquals, + rhs: 'myString' + }) ).toEqual({ field: 'stringAttribute', operator: FilterOperator.NotEquals, @@ -203,9 +181,11 @@ describe('Filter Parser Lookup service', () => { }); expect( - spectator.service - .lookup(FilterOperator.In) - .parseFilterString(getTestFilterAttribute(FilterAttributeType.String), 'String Attribute IN myStr, myString') + spectator.service.lookup(FilterOperator.In).parseSplitFilter({ + attribute: getTestFilterAttribute(FilterAttributeType.String), + operator: FilterOperator.In, + rhs: 'myStr, myString' + }) ).toEqual({ field: 'stringAttribute', operator: FilterOperator.In, @@ -213,119 +193,38 @@ describe('Filter Parser Lookup service', () => { }); expect( - spectator.service - .lookup(FilterOperator.ContainsKey) - .parseFilterString( - getTestFilterAttribute(FilterAttributeType.StringMap), - 'String Map Attribute CONTAINS_KEY myKey' - ) + spectator.service.lookup(FilterOperator.ContainsKey).parseSplitFilter({ + attribute: getTestFilterAttribute(FilterAttributeType.StringMap), + operator: FilterOperator.ContainsKey, + rhs: 'myKey' + }) ).toEqual({ field: 'stringMapAttribute', operator: FilterOperator.ContainsKey, - value: ['myKey'] + value: 'myKey' }); expect( - spectator.service - .lookup(FilterOperator.ContainsKeyValue) - .parseFilterString( - getTestFilterAttribute(FilterAttributeType.StringMap), - 'String Map Attribute CONTAINS_KEY_VALUE myKey:myValue' - ) + spectator.service.lookup(FilterOperator.Equals).parseSplitFilter({ + attribute: getTestFilterAttribute(FilterAttributeType.StringMap), + subpath: 'myKey', + operator: FilterOperator.Equals, + rhs: 'myValue' + }) ).toEqual({ field: 'stringMapAttribute', - operator: FilterOperator.ContainsKeyValue, - value: ['myKey', 'myValue'] - }); - - expect( - spectator.service - .lookup(FilterOperator.Equals) - .parseFilterString(getTestFilterAttribute(FilterAttributeType.Boolean), 'Timestamp Attribute = 1601578015330') - ).toEqual(undefined); - }); - - test('can provide a parser to parse various URL filter strings', () => { - expect( - spectator.service - .lookup(FilterOperator.Equals) - .parseUrlFilterString(getTestFilterAttribute(FilterAttributeType.Boolean), 'booleanAttribute_eq_false') - ).toEqual({ - field: 'booleanAttribute', + subpath: 'myKey', operator: FilterOperator.Equals, - value: false - }); - - expect( - spectator.service - .lookup(FilterOperator.LessThanOrEqualTo) - .parseUrlFilterString(getTestFilterAttribute(FilterAttributeType.Number), 'numberAttribute_lte_217') - ).toEqual({ - field: 'numberAttribute', - operator: FilterOperator.LessThanOrEqualTo, - value: 217 - }); - - expect( - spectator.service - .lookup(FilterOperator.In) - .parseUrlFilterString(getTestFilterAttribute(FilterAttributeType.Number), 'numberAttribute_in_217%2C415%2C707') - ).toEqual({ - field: 'numberAttribute', - operator: FilterOperator.In, - value: [217, 415, 707] - }); - - expect( - spectator.service - .lookup(FilterOperator.NotEquals) - .parseUrlFilterString(getTestFilterAttribute(FilterAttributeType.String), 'stringAttribute_neq_myString') - ).toEqual({ - field: 'stringAttribute', - operator: FilterOperator.NotEquals, - value: 'myString' - }); - - expect( - spectator.service - .lookup(FilterOperator.In) - .parseUrlFilterString(getTestFilterAttribute(FilterAttributeType.String), 'stringAttribute_in_myStr%2CmyString') - ).toEqual({ - field: 'stringAttribute', - operator: FilterOperator.In, - value: ['myStr', 'myString'] - }); - - expect( - spectator.service - .lookup(FilterOperator.ContainsKey) - .parseUrlFilterString(getTestFilterAttribute(FilterAttributeType.StringMap), 'stringMapAttribute_ck_myKey') - ).toEqual({ - field: 'stringMapAttribute', - operator: FilterOperator.ContainsKey, - value: ['myKey'] - }); - - expect( - spectator.service - .lookup(FilterOperator.ContainsKeyValue) - .parseUrlFilterString( - getTestFilterAttribute(FilterAttributeType.StringMap), - 'stringMapAttribute_ckv_myKey%3AmyValue' - ) - ).toEqual({ - field: 'stringMapAttribute', - operator: FilterOperator.ContainsKeyValue, - value: ['myKey', 'myValue'] + value: 'myValue' }); + // Not supported expect( - spectator.service - .lookup(FilterOperator.Equals) - .parseUrlFilterString( - getTestFilterAttribute(FilterAttributeType.Timestamp), - 'timestampAttribute_eq_1601578015330' - ) + spectator.service.lookup(FilterOperator.Equals).parseSplitFilter({ + attribute: getTestFilterAttribute(FilterAttributeType.Boolean), + operator: FilterOperator.Equals, + rhs: '601578015330' + }) ).toEqual(undefined); }); }); 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 df0c15505..b1b215261 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 @@ -11,6 +11,9 @@ import { InFilterParser } from './types/in-filter-parser'; providedIn: 'root' }) 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 { switch (operator) { case FilterOperator.Equals: @@ -24,7 +27,6 @@ export class FilterParserLookupService { case FilterOperator.In: return new InFilterParser(); case FilterOperator.ContainsKey: - case FilterOperator.ContainsKeyValue: return new ContainsFilterParser(); default: return assertUnreachable(operator); diff --git a/projects/components/src/filtering/filter/parser/parsed-filter.ts b/projects/components/src/filtering/filter/parser/parsed-filter.ts index 0b2ba9bea..b63616b3d 100644 --- a/projects/components/src/filtering/filter/parser/parsed-filter.ts +++ b/projects/components/src/filtering/filter/parser/parsed-filter.ts @@ -1,13 +1,82 @@ +import { KeysWithType } from '@hypertrace/common'; +import { FilterAttribute } from '../filter-attribute'; +import { FilterAttributeType } from '../filter-attribute-type'; +import { MAP_LHS_DELIMITER } from '../filter-delimiters'; import { FilterOperator } from '../filter-operators'; export interface ParsedFilter { field: string; + subpath?: string; operator: FilterOperator; value: TValue; } -export interface SplitFilter { - lhs: string; +export interface FilterAttributeExpression { + attribute: FilterAttribute; + subpath?: string; +} + +export interface SplitFilter extends FilterAttributeExpression { operator: TOperator; rhs: string; } + +export const splitFilterStringByOperator = ( + attribute: FilterAttribute, + possibleOperators: TOperator[], + filterString: string +): SplitFilter | undefined => { + const matchingOperator = possibleOperators + .sort((a, b) => b.length - a.length) // Prefer longer matches + .find(op => filterString.includes(op)); + + if (!matchingOperator) { + return undefined; + } + if ( + filterString.endsWith(matchingOperator) && + possibleOperators.filter(operator => operator.startsWith(matchingOperator)).length > 1 + ) { + // If our string ends with the start of multiple operators (e.g. "attr <"), it could continue to a different operator so abort to avoid ambiguity + return undefined; + } + const [lhs, rhs] = filterString.split(matchingOperator).map(str => str.trim()); + const attributeExpression = tryParseStringForAttribute(attribute, lhs, ['displayName', 'name']); + + return attributeExpression && { ...attributeExpression, operator: matchingOperator, rhs: rhs }; +}; + +export const tryParseStringForAttribute = ( + attributeToTest: FilterAttribute, + text: string, + 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()) + ); + if (!matchingNameField) { + return undefined; + } + // Now, we know that it does match. Remove the attribute name from the beginning and try to determine the subpath next. + const stringAfterAttributeName = text.slice(attributeToTest[matchingNameField].length).trim(); + + if (stringAfterAttributeName.startsWith(MAP_LHS_DELIMITER)) { + if (attributeToTest.type !== FilterAttributeType.StringMap) { + // Can't have a subpath if not a map + return undefined; + } + const potentialSubpath = stringAfterAttributeName.slice(MAP_LHS_DELIMITER.length); + // Subpaths support alphanumeric, -, - and . characters + const firstNonSubpathCharacterIndex = potentialSubpath.search(/[^\w\-\.]/); + const subpath = + firstNonSubpathCharacterIndex === -1 + ? potentialSubpath + : potentialSubpath.slice(0, firstNonSubpathCharacterIndex); + + return { attribute: attributeToTest, subpath: subpath }; + } + + return { attribute: attributeToTest }; +}; diff --git a/projects/components/src/filtering/filter/parser/types/abstract-filter-parser.test.ts b/projects/components/src/filtering/filter/parser/types/abstract-filter-parser.test.ts deleted file mode 100644 index f0dfa91dd..000000000 --- a/projects/components/src/filtering/filter/parser/types/abstract-filter-parser.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { FilterAttribute, FilterAttributeType } from '@hypertrace/components'; -import { getTestFilterAttribute } from '@hypertrace/test-utils'; -import { ParsedFilter } from '../parsed-filter'; -import { ContainsFilterParser } from './contains-filter-parser'; - -describe('Filter Parser', () => { - test('correctly parses CONTAINS_KEY with dot', () => { - const attribute: FilterAttribute = getTestFilterAttribute(FilterAttributeType.StringMap); - const filterString = 'String Map Attribute CONTAINS_KEY http.url'; - - const containsFilterParser: ContainsFilterParser = new ContainsFilterParser(); - const parsedFilter: ParsedFilter | undefined = containsFilterParser.parseFilterString( - attribute, - filterString - ); - - expect(parsedFilter).toEqual({ - field: 'stringMapAttribute', - operator: 'CONTAINS_KEY', - value: ['http.url'] - }); - }); - - test('correctly parses CONTAINS_KEY with colon', () => { - const attribute: FilterAttribute = getTestFilterAttribute(FilterAttributeType.StringMap); - const filterString = 'String Map Attribute CONTAINS_KEY http:url'; - - const containsFilterParser: ContainsFilterParser = new ContainsFilterParser(); - const parsedFilter: ParsedFilter | undefined = containsFilterParser.parseFilterString( - attribute, - filterString - ); - - expect(parsedFilter).toEqual({ - field: 'stringMapAttribute', - operator: 'CONTAINS_KEY', - value: ['http:url'] - }); - }); - - test('correctly parses CONTAINS_KEY_VALUE for tag with dot and URL', () => { - const attribute: FilterAttribute = getTestFilterAttribute(FilterAttributeType.StringMap); - const filterString = - 'String Map Attribute.http.url CONTAINS_KEY_VALUE http://dataservice:9394/userreview?productId=f2620500-8b55-4fab-b7a2-fe8af6f5ae24'; - - const containsFilterParser: ContainsFilterParser = new ContainsFilterParser(); - const parsedFilter: ParsedFilter | undefined = containsFilterParser.parseFilterString( - attribute, - filterString - ); - - expect(parsedFilter).toEqual({ - field: 'stringMapAttribute', - operator: 'CONTAINS_KEY_VALUE', - value: ['http.url', 'http://dataservice:9394/userreview?productId=f2620500-8b55-4fab-b7a2-fe8af6f5ae24'] - }); - }); - - test('correctly parses CONTAINS_KEY_VALUE for tag with colon and URL', () => { - const attribute: FilterAttribute = getTestFilterAttribute(FilterAttributeType.StringMap); - const filterString = - 'String Map Attribute.http:url CONTAINS_KEY_VALUE http://dataservice:9394/userreview?productId=f2620500-8b55-4fab-b7a2-fe8af6f5ae24'; - - const containsFilterParser: ContainsFilterParser = new ContainsFilterParser(); - const parsedFilter: ParsedFilter | undefined = containsFilterParser.parseFilterString( - attribute, - filterString - ); - - expect(parsedFilter).toEqual({ - field: 'stringMapAttribute', - operator: 'CONTAINS_KEY_VALUE', - value: ['http:url', 'http://dataservice:9394/userreview?productId=f2620500-8b55-4fab-b7a2-fe8af6f5ae24'] - }); - }); - - test('correctly parses CONTAINS_KEY_VALUE for tag with colon and empty string', () => { - const attribute: FilterAttribute = getTestFilterAttribute(FilterAttributeType.StringMap); - const filterString = 'String Map Attribute.http:url CONTAINS_KEY_VALUE ""'; - const containsFilterParser: ContainsFilterParser = new ContainsFilterParser(); - const parsedFilter: ParsedFilter | undefined = containsFilterParser.parseFilterString( - attribute, - filterString - ); - - expect(parsedFilter).toEqual({ - field: 'stringMapAttribute', - operator: 'CONTAINS_KEY_VALUE', - value: ['http:url', '""'] - }); - }); -}); diff --git a/projects/components/src/filtering/filter/parser/types/abstract-filter-parser.ts b/projects/components/src/filtering/filter/parser/types/abstract-filter-parser.ts index 8dbbf594f..fb6139430 100644 --- a/projects/components/src/filtering/filter/parser/types/abstract-filter-parser.ts +++ b/projects/components/src/filtering/filter/parser/types/abstract-filter-parser.ts @@ -1,94 +1,26 @@ -import { FilterAttribute } from '../../filter-attribute'; import { FilterAttributeType } from '../../filter-attribute-type'; -import { FilterOperator, fromUrlFilterOperator, toUrlFilterOperator, UrlFilterOperator } from '../../filter-operators'; +import { FilterOperator } from '../../filter-operators'; import { ParsedFilter, SplitFilter } from '../parsed-filter'; export abstract class AbstractFilterParser { public abstract supportedAttributeTypes(): FilterAttributeType[]; public abstract supportedOperators(): FilterOperator[]; - public parseNameString(attribute: FilterAttribute, splitFilter: SplitFilter): string | undefined { - return attribute.displayName.toLowerCase() !== splitFilter.lhs.toLowerCase() ? undefined : attribute.name; - } - - public abstract parseValueString( - attribute: FilterAttribute, - splitFilter: SplitFilter - ): TValue | undefined; - - public parseFilterString(attribute: FilterAttribute, filterString: string): ParsedFilter | undefined { - const splitFilter: SplitFilter | undefined = splitFilterStringByOperator( - this.supportedOperators(), - filterString, - true - ); - - return splitFilter === undefined ? undefined : this.parseSplitFilter(attribute, splitFilter); - } - - public parseUrlFilterString(attribute: FilterAttribute, urlFilterString: string): ParsedFilter | undefined { - const splitUrlFilter: SplitFilter | undefined = splitFilterStringByOperator( - this.supportedOperators().map(toUrlFilterOperator), - decodeURIComponent(urlFilterString), - false - ); - - return splitUrlFilter !== undefined - ? this.parseSplitFilter(attribute, { - lhs: attribute.displayName, - operator: fromUrlFilterOperator(splitUrlFilter.operator), - rhs: splitUrlFilter.rhs - }) - : undefined; - } - - private parseSplitFilter( - attribute: FilterAttribute, - splitFilter: SplitFilter - ): ParsedFilter | undefined { - const parsedName = this.parseNameString(attribute, splitFilter); + public abstract parseValueString(splitFilter: SplitFilter): TValue | undefined; - if (parsedName === undefined) { - // Unable to parse attribute from lhs - return undefined; - } - - const parsedValue = this.parseValueString(attribute, splitFilter); + public parseSplitFilter(splitFilter: SplitFilter): ParsedFilter | undefined { + const parsedValue = this.parseValueString(splitFilter); if (parsedValue === undefined) { // Unable to parse value from rhs return undefined; } - // Successfully parsed URL filter - return { - field: attribute.name, + field: splitFilter.attribute.name, + subpath: splitFilter.subpath, operator: splitFilter.operator, value: parsedValue }; } } - -export const splitFilterStringByOperator = ( - possibleOperators: TOperator[], - filterString: string, - expectSpaceAroundOperator: boolean = true -): SplitFilter | undefined => { - const matchingOperator = possibleOperators - .sort((o1: string, o2: string) => o2.length - o1.length) // Sort by length to check multichar ops first - .map(op => (expectSpaceAroundOperator ? ` ${op as string} ` : op)) - .find(op => filterString.includes(op)); - - if (matchingOperator === undefined) { - return undefined; - } - - const parts = filterString.split(matchingOperator).map(str => str.trim()); - - return { - lhs: parts[0], - operator: matchingOperator.trim() as TOperator, - rhs: parts[1] - }; -}; diff --git a/projects/components/src/filtering/filter/parser/types/comparison-filter-parser.ts b/projects/components/src/filtering/filter/parser/types/comparison-filter-parser.ts index eb5a841e5..fb9b7e773 100644 --- a/projects/components/src/filtering/filter/parser/types/comparison-filter-parser.ts +++ b/projects/components/src/filtering/filter/parser/types/comparison-filter-parser.ts @@ -1,5 +1,4 @@ import { assertUnreachable } from '@hypertrace/common'; -import { FilterAttribute } from '../../filter-attribute'; import { FilterAttributeType } from '../../filter-attribute-type'; import { FilterOperator } from '../../filter-operators'; import { SplitFilter } from '../parsed-filter'; @@ -7,7 +6,12 @@ import { AbstractFilterParser } from './abstract-filter-parser'; export class ComparisonFilterParser extends AbstractFilterParser { public supportedAttributeTypes(): FilterAttributeType[] { - return [FilterAttributeType.Boolean, FilterAttributeType.Number, FilterAttributeType.String]; + return [ + FilterAttributeType.Boolean, + FilterAttributeType.Number, + FilterAttributeType.String, + FilterAttributeType.StringMap + ]; } public supportedOperators(): FilterOperator[] { @@ -22,23 +26,20 @@ export class ComparisonFilterParser extends AbstractFilterParser - ): PossibleValuesTypes | undefined { - switch (attribute.type) { + public parseValueString(splitFilter: SplitFilter): PossibleValuesTypes | undefined { + switch (splitFilter.attribute.type) { case FilterAttributeType.Boolean: return this.parseBooleanValue(splitFilter.rhs); case FilterAttributeType.Number: return this.parseNumberValue(splitFilter.rhs); case FilterAttributeType.String: + case FilterAttributeType.StringMap: return this.parseStringValue(splitFilter.rhs); case FilterAttributeType.StringArray: // Unsupported - case FilterAttributeType.StringMap: // Unsupported case FilterAttributeType.Timestamp: // Unsupported return undefined; default: - assertUnreachable(attribute.type); + assertUnreachable(splitFilter.attribute.type); } } diff --git a/projects/components/src/filtering/filter/parser/types/contains-filter-parser.ts b/projects/components/src/filtering/filter/parser/types/contains-filter-parser.ts index a7a88ddf9..afe37b05e 100644 --- a/projects/components/src/filtering/filter/parser/types/contains-filter-parser.ts +++ b/projects/components/src/filtering/filter/parser/types/contains-filter-parser.ts @@ -1,35 +1,22 @@ -import { assertUnreachable, isNonEmptyString } from '@hypertrace/common'; -import { FilterAttribute } from '../../filter-attribute'; +import { assertUnreachable } from '@hypertrace/common'; import { FilterAttributeType } from '../../filter-attribute-type'; -import { MAP_LHS_DELIMITER, MAP_RHS_DELIMITER } from '../../filter-delimiters'; import { FilterOperator } from '../../filter-operators'; import { SplitFilter } from '../parsed-filter'; import { AbstractFilterParser } from './abstract-filter-parser'; -export class ContainsFilterParser extends AbstractFilterParser { +export class ContainsFilterParser extends AbstractFilterParser { public supportedAttributeTypes(): FilterAttributeType[] { return [FilterAttributeType.StringMap]; } public supportedOperators(): FilterOperator[] { - return [FilterOperator.ContainsKey, FilterOperator.ContainsKeyValue]; + return [FilterOperator.ContainsKey]; } - public parseNameString(attribute: FilterAttribute, splitFilter: SplitFilter): string | undefined { - const splitLhs = this.splitLhs(attribute, splitFilter); - - return splitLhs === undefined - ? undefined - : super.parseNameString(attribute, { ...splitFilter, lhs: splitLhs.displayName }); - } - - public parseValueString( - attribute: FilterAttribute, - splitFilter: SplitFilter - ): PossibleValuesTypes | undefined { - switch (attribute.type) { + public parseValueString(splitFilter: SplitFilter): string | undefined { + switch (splitFilter.attribute.type) { case FilterAttributeType.StringMap: - return this.parseStringMapValue(attribute, splitFilter); + return String(splitFilter.rhs); case FilterAttributeType.StringArray: // Unsupported case FilterAttributeType.Number: // Unsupported case FilterAttributeType.Boolean: // Unsupported @@ -37,64 +24,7 @@ export class ContainsFilterParser extends AbstractFilterParser - ): string[] | undefined { - if (splitFilter.lhs === attribute.displayName) { - switch (splitFilter.operator) { - case FilterOperator.ContainsKey: - return [splitFilter.rhs]; - case FilterOperator.ContainsKeyValue: - return splitFirstOccurrenceOmitEmpty(splitFilter.rhs, MAP_RHS_DELIMITER); - case FilterOperator.Equals: - case FilterOperator.NotEquals: - case FilterOperator.LessThan: - case FilterOperator.LessThanOrEqualTo: - case FilterOperator.GreaterThan: - case FilterOperator.GreaterThanOrEqualTo: - case FilterOperator.Like: - case FilterOperator.In: - return undefined; - default: - assertUnreachable(splitFilter.operator); - } - } - - const splitLhs = this.splitLhs(attribute, splitFilter); - - return splitLhs === undefined || splitLhs.key === undefined ? undefined : [splitLhs.key, splitFilter.rhs]; - } - - private splitLhs(attribute: FilterAttribute, splitFilter: SplitFilter): SplitLhs | undefined { - if (splitFilter.lhs === attribute.displayName) { - return { displayName: attribute.displayName }; + return assertUnreachable(splitFilter.attribute.type); } - - const parts = splitFilter.lhs.split(MAP_LHS_DELIMITER); - - return parts.length < 2 - ? undefined - : { - displayName: attribute.displayName, - key: parts.slice(1).join(MAP_LHS_DELIMITER) - }; } } - -type PossibleValuesTypes = string[]; - -interface SplitLhs { - displayName: string; - key?: string; -} - -const splitFirstOccurrenceOmitEmpty = (str: string, delimiter: string): string[] => { - const firstIndex = str.indexOf(delimiter); - - return [str.substr(0, firstIndex), str.substr(firstIndex + 1)].filter(isNonEmptyString); -}; diff --git a/projects/components/src/filtering/filter/parser/types/in-filter-parser.ts b/projects/components/src/filtering/filter/parser/types/in-filter-parser.ts index 62e052958..b97575d5c 100644 --- a/projects/components/src/filtering/filter/parser/types/in-filter-parser.ts +++ b/projects/components/src/filtering/filter/parser/types/in-filter-parser.ts @@ -1,5 +1,4 @@ import { assertUnreachable } from '@hypertrace/common'; -import { FilterAttribute } from '../../filter-attribute'; import { FilterAttributeType } from '../../filter-attribute-type'; import { ARRAY_DELIMITER } from '../../filter-delimiters'; import { FilterOperator } from '../../filter-operators'; @@ -8,29 +7,26 @@ import { AbstractFilterParser } from './abstract-filter-parser'; export class InFilterParser extends AbstractFilterParser { public supportedAttributeTypes(): FilterAttributeType[] { - return [FilterAttributeType.String, FilterAttributeType.Number]; + return [FilterAttributeType.String, FilterAttributeType.Number, FilterAttributeType.StringMap]; } public supportedOperators(): FilterOperator[] { return [FilterOperator.In]; } - public parseValueString( - attribute: FilterAttribute, - splitFilter: SplitFilter - ): PossibleValuesTypes | undefined { - switch (attribute.type) { + public parseValueString(splitFilter: SplitFilter): PossibleValuesTypes | undefined { + switch (splitFilter.attribute.type) { case FilterAttributeType.String: + case FilterAttributeType.StringMap: return this.parseStringArrayValue(splitFilter.rhs); case FilterAttributeType.Number: return this.parseNumberArrayValue(splitFilter.rhs); case FilterAttributeType.Boolean: // Unsupported case FilterAttributeType.StringArray: // Unsupported - case FilterAttributeType.StringMap: // Unsupported case FilterAttributeType.Timestamp: // Unsupported return undefined; default: - assertUnreachable(attribute.type); + assertUnreachable(splitFilter.attribute.type); } } diff --git a/projects/observability/src/pages/explorer/explorer-service.ts b/projects/observability/src/pages/explorer/explorer-service.ts index 004444d05..be24a5982 100644 --- a/projects/observability/src/pages/explorer/explorer-service.ts +++ b/projects/observability/src/pages/explorer/explorer-service.ts @@ -31,7 +31,7 @@ export class ExplorerService { filterAttribute => this.filterBuilderLookupService .lookup(filterAttribute.type) - .buildFilter(filterAttribute, filter.operator, filter.value).urlString + .buildFilter(filterAttribute, filter.operator, filter.value, filter.subpath).urlString ) ) ); diff --git a/projects/observability/src/pages/explorer/explorer.component.test.ts b/projects/observability/src/pages/explorer/explorer.component.test.ts index 617e82718..fb1c871e8 100644 --- a/projects/observability/src/pages/explorer/explorer.component.test.ts +++ b/projects/observability/src/pages/explorer/explorer.component.test.ts @@ -203,7 +203,7 @@ describe('Explorer component', () => { expect.objectContaining({ requestType: EXPLORE_GQL_REQUEST, context: ObservabilityTraceType.Api, - filters: [new GraphQlFieldFilter('first', GraphQlOperatorType.Equals, 'foo')], + filters: [new GraphQlFieldFilter({ key: 'first' }, GraphQlOperatorType.Equals, 'foo')], limit: 1000, interval: new TimeDuration(15, TimeUnit.Second) }), @@ -214,7 +214,7 @@ describe('Explorer component', () => { 2, expect.objectContaining({ requestType: TRACES_GQL_REQUEST, - filters: [new GraphQlFieldFilter('first', GraphQlOperatorType.Equals, 'foo')], + filters: [new GraphQlFieldFilter({ key: 'first' }, GraphQlOperatorType.Equals, 'foo')], limit: 100 }), expect.objectContaining({}) @@ -290,7 +290,7 @@ describe('Explorer component', () => { context: SPAN_SCOPE, limit: 1000, interval: new TimeDuration(15, TimeUnit.Second), - filters: [new GraphQlFieldFilter('first', GraphQlOperatorType.Equals, 'foo')] + filters: [new GraphQlFieldFilter({ key: 'first' }, GraphQlOperatorType.Equals, 'foo')] }), expect.objectContaining({}) ); @@ -300,7 +300,7 @@ describe('Explorer component', () => { expect.objectContaining({ requestType: SPANS_GQL_REQUEST, limit: 100, - filters: [new GraphQlFieldFilter('first', GraphQlOperatorType.Equals, 'foo')] + filters: [new GraphQlFieldFilter({ key: 'first' }, GraphQlOperatorType.Equals, 'foo')] }), expect.objectContaining({}) ); diff --git a/projects/observability/src/shared/components/span-detail/tags/span-tags-detail.component.ts b/projects/observability/src/shared/components/span-detail/tags/span-tags-detail.component.ts index fdf77a6c0..5d574940a 100644 --- a/projects/observability/src/shared/components/span-detail/tags/span-tags-detail.component.ts +++ b/projects/observability/src/shared/components/span-detail/tags/span-tags-detail.component.ts @@ -44,7 +44,7 @@ export class SpanTagsDetailComponent implements OnChanges { public getExploreNavigationParams = (tag: ListViewRecord): Observable => this.explorerService.buildNavParamsWithFilters(ScopeQueryParam.EndpointTraces, [ - { field: 'tags', operator: FilterOperator.ContainsKeyValue, value: [tag.key, tag.value] } + { field: 'tags', subpath: tag.key, operator: FilterOperator.Equals, value: tag.value } ]); private buildTagRecords(): void { diff --git a/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.test.ts b/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.test.ts index a80fd0f27..c4d9c4d73 100644 --- a/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.test.ts +++ b/projects/observability/src/shared/dashboard/dashboard-wrapper/navigable-dashboard.component.test.ts @@ -144,7 +144,7 @@ describe('Navigable dashboard component', () => { }; spectator.query(FilterBarComponent)?.filtersChange.next([explicitFilter]); expect(mockDataSource.addFilters).toHaveBeenCalledWith( - expect.objectContaining({ keyOrExpression: 'foo', operator: GraphQlOperatorType.Equals, value: 'bar' }) + expect.objectContaining({ keyOrExpression: { key: 'foo' }, operator: GraphQlOperatorType.Equals, value: 'bar' }) ); }); diff --git a/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-id-scope-filter.model.ts b/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-id-scope-filter.model.ts index 74facbe06..65254749a 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-id-scope-filter.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-id-scope-filter.model.ts @@ -33,9 +33,7 @@ export class GraphqlIdScopeFilterModel implements GraphQlFilter { GraphQlOperatorType.GreaterThanOrEqualTo, GraphQlOperatorType.LessThan, GraphQlOperatorType.LessThanOrEqualTo, - GraphQlOperatorType.Like, - GraphQlOperatorType.ContainsKey, - GraphQlOperatorType.ContainsKeyValue + GraphQlOperatorType.Like ] } as EnumPropertyTypeInstance }) diff --git a/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-key-value-filter.model.ts b/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-key-value-filter.model.ts index 45964c99f..664d72999 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-key-value-filter.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/filter/graphql-key-value-filter.model.ts @@ -31,8 +31,7 @@ export class GraphQlKeyValueFilterModel implements GraphQlFilter { GraphQlOperatorType.LessThanOrEqualTo, GraphQlOperatorType.Like, GraphQlOperatorType.NotIn, - GraphQlOperatorType.ContainsKey, - GraphQlOperatorType.ContainsKeyValue + GraphQlOperatorType.ContainsKey ] } as EnumPropertyTypeInstance }) diff --git a/projects/observability/src/shared/graphql/model/schema/filter/graphql-filter.ts b/projects/observability/src/shared/graphql/model/schema/filter/graphql-filter.ts index c19cff0c7..899bf40af 100644 --- a/projects/observability/src/shared/graphql/model/schema/filter/graphql-filter.ts +++ b/projects/observability/src/shared/graphql/model/schema/filter/graphql-filter.ts @@ -23,6 +23,5 @@ export const enum GraphQlOperatorType { Like = 'LIKE', In = 'IN', NotIn = 'NOT_IN', - ContainsKey = 'CONTAINS_KEY', - ContainsKeyValue = 'CONTAINS_KEY_VALUE' + ContainsKey = 'CONTAINS_KEY' } 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 5af3c9b65..213376efe 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 @@ -41,12 +41,12 @@ describe('Graphql filter builder service', () => { buildFilter(attribute1, FilterOperator.LessThanOrEqualTo, 20) ]) ).toEqual([ - new GraphQlFieldFilter(attribute2.name, GraphQlOperatorType.Equals, 'foo'), - new GraphQlFieldFilter(attribute2.name, GraphQlOperatorType.NotEquals, 'bar'), - new GraphQlFieldFilter(attribute1.name, GraphQlOperatorType.GreaterThan, 5), - new GraphQlFieldFilter(attribute1.name, GraphQlOperatorType.GreaterThanOrEqualTo, 10), - new GraphQlFieldFilter(attribute1.name, GraphQlOperatorType.LessThan, 15), - new GraphQlFieldFilter(attribute1.name, GraphQlOperatorType.LessThanOrEqualTo, 20) + new GraphQlFieldFilter({ key: attribute2.name }, GraphQlOperatorType.Equals, 'foo'), + new GraphQlFieldFilter({ key: attribute2.name }, GraphQlOperatorType.NotEquals, 'bar'), + new GraphQlFieldFilter({ key: attribute1.name }, GraphQlOperatorType.GreaterThan, 5), + new GraphQlFieldFilter({ key: attribute1.name }, GraphQlOperatorType.GreaterThanOrEqualTo, 10), + new GraphQlFieldFilter({ key: attribute1.name }, GraphQlOperatorType.LessThan, 15), + new GraphQlFieldFilter({ key: attribute1.name }, GraphQlOperatorType.LessThanOrEqualTo, 20) ]); }); }); 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 608c248d9..cc5d0bf47 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 @@ -10,7 +10,11 @@ export class GraphQlFilterBuilderService { public buildGraphQlFilters(filters: TableFilter[]): GraphQlFilter[] { return filters.map( filter => - new GraphQlFieldFilter(filter.field, toGraphQlOperator(filter.operator), filter.value as GraphQlArgumentValue) + new GraphQlFieldFilter( + { key: filter.field, subpath: filter.subpath }, + toGraphQlOperator(filter.operator), + filter.value as GraphQlArgumentValue + ) ); } } @@ -35,8 +39,6 @@ export const toGraphQlOperator = (operator: FilterOperator): GraphQlOperatorType return GraphQlOperatorType.In; case FilterOperator.ContainsKey: return GraphQlOperatorType.ContainsKey; - case FilterOperator.ContainsKeyValue: - return GraphQlOperatorType.ContainsKeyValue; default: return assertUnreachable(operator); }