Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FilterValue } from './../../filter/filter';
import { Injectable } from '@angular/core';
import { isEmpty } from 'lodash-es';
import { FilterBuilderLookupService } from '../../filter/builder/filter-builder-lookup.service';
Expand Down Expand Up @@ -61,7 +62,7 @@ export class FilterChipService {

private buildIncompleteFiltersForAttribute(
text: string,
filterBuilder: AbstractFilterBuilder<unknown>,
filterBuilder: AbstractFilterBuilder<FilterValue>,
attributeExpression: FilterAttributeExpression
): IncompleteFilter[] {
const topLevelOperatorFilters = filterBuilder.supportedTopLevelOperators().map(operator => ({
Expand Down Expand Up @@ -95,8 +96,8 @@ export class FilterChipService {
}

private buildIncompleteFilterForAttributeAndOperator(
filterBuilder: AbstractFilterBuilder<unknown>,
filterParser: AbstractFilterParser<unknown>,
filterBuilder: AbstractFilterBuilder<FilterValue>,
filterParser: AbstractFilterParser<FilterValue>,
splitFilter: SplitFilter<FilterOperator>,
text: string
): IncompleteFilter {
Expand Down Expand Up @@ -132,7 +133,7 @@ export class FilterChipService {
}

private buildIncompleteFilterForPartialAttributeMatch(
filterBuilder: AbstractFilterBuilder<unknown>,
filterBuilder: AbstractFilterBuilder<FilterValue>,
attribute: FilterAttribute
): IncompleteFilter {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -45,7 +45,7 @@ export class FilterButtonComponent implements OnChanges {
public attribute?: FilterAttribute;

@Input()
public value?: unknown;
public value?: FilterValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems completely dependent on the filter implementation - what's the point of typing it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required for Filter to GqlFilter transformation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the kind of thing I was saying the other day we shouldn't try to make generic, because there is no one GQL filter representation.


@Output()
public readonly popoverOpen: EventEmitter<boolean> = new EventEmitter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,7 +43,7 @@ import { FilterUrlService } from '../filter/filter-url.service';
})
export class InFilterModalComponent {
public isSupported: boolean = false;
public selected: Set<unknown> = new Set<unknown>();
public selected: Set<FilterValue> = new Set<FilterValue>();

public constructor(
private readonly modalRef: ModalRef<never>,
Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,7 +12,7 @@ import { StringMapFilterBuilder } from './types/string-map-filter-builder';
providedIn: 'root'
})
export class FilterBuilderLookupService {
public lookup(type: FilterAttributeType): AbstractFilterBuilder<unknown> {
public lookup(type: FilterAttributeType): AbstractFilterBuilder<FilterValue> {
switch (type) {
case FilterAttributeType.Boolean:
return new BooleanFilterBuilder();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { collapseWhitespace } from '@hypertrace/common';
import { isEmpty } from 'lodash-es';
import { Filter } from '../../filter';
import { Filter, FilterValue } 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<TValue> {
export abstract class AbstractFilterBuilder<TValue extends FilterValue> {
public abstract supportedAttributeType(): FilterAttributeType;

public abstract supportedSubpathOperators(): FilterOperator[];
Expand Down
20 changes: 17 additions & 3 deletions projects/components/src/filtering/filter/filter-operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const enum FilterOperator {
GreaterThanOrEqualTo = '>=',
Like = '~',
In = 'IN',
NotIn = 'NOT_IN',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this isn't supported, if it's being added please put in a separate PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw one operator in Gql. So thought it is supported.

yes, this is just a draft PR. I will create smaller ones when it is ready

ContainsKey = 'CONTAINS_KEY'
}

Expand All @@ -21,6 +22,7 @@ export const enum UrlFilterOperator {
GreaterThanOrEqualTo = '_gte_',
Like = '_lk_',
In = '_in_',
NotIn = '_nin_',
ContainsKey = '_ck_'
}

Expand All @@ -44,6 +46,8 @@ export const toUrlFilterOperator = (operator: FilterOperator): UrlFilterOperator
return UrlFilterOperator.In;
case FilterOperator.ContainsKey:
return UrlFilterOperator.ContainsKey;
case FilterOperator.NotIn:
return UrlFilterOperator.ContainsKey;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad map

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. Do you know we need three operator types? GqlOperators, FilterOperator and URLFilterOperator. Imo We can combine FilterOperator and URLFilterOperator and only use one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's three different string representations of a user - the server's value (e.g. EQUALS), the value we display in the UI (=) and the value we use in a URL string where we can't use all specials chars (_eq_). We probably could try to consolidate such as by url encoding or similar, but seemed easy enough to maintain initially.

default:
return assertUnreachable(operator);
}
Expand All @@ -67,6 +71,8 @@ export const fromUrlFilterOperator = (operator: UrlFilterOperator): FilterOperat
return FilterOperator.Like;
case UrlFilterOperator.In:
return FilterOperator.In;
case UrlFilterOperator.NotIn:
return FilterOperator.NotIn;
case UrlFilterOperator.ContainsKey:
return FilterOperator.ContainsKey;
default:
Expand All @@ -78,6 +84,7 @@ export const incompatibleOperators = (operator: FilterOperator): FilterOperator[
switch (operator) {
case FilterOperator.In:
case FilterOperator.Equals:
case FilterOperator.NotIn:
return [
FilterOperator.In,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic seems off and is not symmetric. Not in would return in is incompatible, but in would not return not in as incompatible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I am not sure what this is code is doing. Still playing around with it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100%, but I believe this is used when adding filters via table controls, so if you do something like add an equals filter it replaces any current one.

FilterOperator.Equals,
Expand All @@ -92,17 +99,24 @@ export const incompatibleOperators = (operator: FilterOperator): FilterOperator[
case FilterOperator.ContainsKey:
case FilterOperator.NotEquals:
case FilterOperator.Like:
return [FilterOperator.In, FilterOperator.Equals];
return [FilterOperator.In, FilterOperator.Equals, FilterOperator.NotIn];
case FilterOperator.LessThan:
case FilterOperator.LessThanOrEqualTo:
return [FilterOperator.In, FilterOperator.Equals, FilterOperator.LessThan, FilterOperator.LessThanOrEqualTo];
return [
FilterOperator.In,
FilterOperator.Equals,
FilterOperator.LessThan,
FilterOperator.LessThanOrEqualTo,
FilterOperator.NotIn
];
case FilterOperator.GreaterThan:
case FilterOperator.GreaterThanOrEqualTo:
return [
FilterOperator.In,
FilterOperator.Equals,
FilterOperator.GreaterThan,
FilterOperator.GreaterThanOrEqualTo
FilterOperator.GreaterThanOrEqualTo,
FilterOperator.NotIn
];
default:
assertUnreachable(operator);
Expand Down
9 changes: 6 additions & 3 deletions projects/components/src/filtering/filter/filter.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { Dictionary } from '@hypertrace/common';
import { FilterAttribute } from './filter-attribute';
import { FilterOperator, incompatibleOperators } from './filter-operators';

export interface Filter<TValue = unknown> extends IncompleteFilter {
export interface Filter<TValue extends FilterValue = FilterValue> extends IncompleteFilter {
operator: FilterOperator;
value: TValue;
urlString: string;
}

export interface IncompleteFilter<TValue = unknown> extends FieldFilter<TValue> {
export interface IncompleteFilter<TValue extends FilterValue = FilterValue> extends FieldFilter<TValue> {
metadata: FilterAttribute;
userString: string;
}

export interface FieldFilter<TValue = unknown> {
export interface FieldFilter<TValue extends FilterValue = FilterValue> {
field: string;
subpath?: string;
operator?: FilterOperator;
value?: TValue;
}

export type FilterValue = string | number | boolean | Date | Dictionary<FilterValue> | FilterValue[];

export const areCompatibleFilters = (f1: Filter, f2: Filter) =>
f1.field !== f2.field || f1.subpath !== f2.subpath || !incompatibleOperators(f1.operator).includes(f2.operator);
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<unknown> {
public lookup(operator: FilterOperator): AbstractFilterParser<FilterValue> {
switch (operator) {
case FilterOperator.Equals:
case FilterOperator.NotEquals:
Expand All @@ -28,6 +29,8 @@ export class FilterParserLookupService {
return new InFilterParser();
case FilterOperator.ContainsKey:
return new ContainsFilterParser();
case FilterOperator.NotIn:
throw new Error('NotIn is not supported');
default:
return assertUnreachable(operator);
}
Expand Down
4 changes: 2 additions & 2 deletions projects/components/src/table/table-api.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -60,7 +60,7 @@ export interface RowStateChange {

export interface TableFilter extends FieldFilter {
operator: FilterOperator;
value: unknown;
value: FilterValue;
}

export const enum TableSortDirection {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,7 +17,7 @@ export class EntitiesAttributeOptionsDataSourceModel extends EntitiesAttributeDa
metaValue: {
field: this.specification.name,
operator: FilterOperator.Equals,
value: value
value: value as FilterValue
}
}))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ export abstract class TableDataSourceModel extends GraphQlDataSourceModel<TableD
): TableDataResponse<TableRow>;

protected toGraphQlFilters(tableFilters: TableFilter[] = []): GraphQlFilter[] {
return this.graphQlFilterBuilderService.buildGraphQlFilters(tableFilters);
return this.graphQlFilterBuilderService.buildGraphQlFiltersFromTableFilters(tableFilters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface AttributeMetadata {
units: string;
type: AttributeMetadataType;
scope: string;
category?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not generally applicable nor part of GQL model, please remove

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The category would be added to attributes eventually, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe so - I thought the conclusion there was that concept was unique to a particular UI page and could be maintained there. It's not a universal concept across all attributes, nor is it necessarily universal that the same attribute belongs to the same category in all contexts - its use case specific.

onlySupportsAggregation: boolean;
onlySupportsGrouping: boolean;
allowedAggregations: MetricAggregationType[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
import { FieldFilter, FilterValue } from './../../../../../components/src/filtering/filter/filter';
import { TableFilter } from './../../../../../components/src/table/table-api';
import { Injectable } from '@angular/core';
import { assertUnreachable } from '@hypertrace/common';
import { FilterOperator, TableFilter } from '@hypertrace/components';
import { FilterOperator } 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[] {
public buildFiltersFromGraphQlFieldFilters(filters: GraphQlFieldFilter[]): FieldFilter[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to convert back from GQL filters to field filters? If we're doing refactoring, we should change any components using GQL filters to use the UI construct instead of supporting going bidrectionally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so that would be a big change. If we want to do it in iterations then temporarily this method will be used in the basic filter bar for now. Slowly, we will migrate the filters to the UI construct.

We would also need to revisit the relationship between IncompleteFilter, FieldFilter, and Filter?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I admittedly have limited context here, but rather than updating the existing basic filter bar component in place, it seems like creating a new one and deprecating the old might be cleaner for a number of reasons - this included. We would save ourselves from introducing new tech debt, as well as bending over backwards to support compatibility with existing usages like the preferences stuff you brought up the other day.

We would also need to revisit the relationship between IncompleteFilter, FieldFilter, and Filter?

Oh, please do! The concept of IncompleteFilter should be internal to any free-text filter input. I'm not sure how I'd design the other interfaces, I think I would probably start the exercise with just Filter as a completely empty interface, parameterize components that use filters and see where some shared understanding is needed (maybe just a text representation of it?).

It all depends on how deep you want to go with this, but if you're going to work with the generic stuff, we should try to get it right rather than keep digging this hole (and if doing this, I would strongly suggest doing it parallel to the existing filters, as mentioned before, rather than in place). If we don't want to take that up now, should create use-case specific components.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disclaimer: I haven't gone through the code yet and have only read the comments.

But what I'm gathering from the comments is that we are trying to decide on meshing the new filtering system with the old one vs just making a clean break and building a new one. I think we all agree deprecating the old one is the right thing to do long term. Lots of history of patching up the old one for new use cases. We know a lot more about all of our use cases and we can code this much better to accommodate the full picture now.

That said, it's always a balance of priorities and timelines. @anandtiwary do you have a sense of how long @aaron-steinfeld's suggestions might take? Are there some things we can do now and phase in the rest of the improvements over time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Things I am re-using:

  1. Filter Objects and various filter builders
  2. Url string to Filter parsing logic
  3. Basic filter bar value auto-completion

Things I am changing:

  1. Moving UI side code to depend on Filter objects instead of GraphQlFilters. We will use a method that would have the conversion logic. This migration will have in phases and we can track it with stories.
  2. Fix the dependency of Filter, FieldFilter, and IncompleteFilter
  3. Using new design for basic view

@jake-bassett Migrating to UI construct (point 1) has a much bigger blast radius. We should do it in stages. Let's wait for other higher-priority tasks to get completed first.

Copy link
Contributor

@jake-bassett jake-bassett Feb 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, got it. Yeah, the items you are re-using are in desperate need of rework but I know that's not at all trivial. I'm good with doing this in phases. As much as I know that code is hard to work with, and making things a bit of a challenge, I don't think we can wait on these filter changes much longer.

I vote for not making this PR any larger than what it already is and revisiting replacing the rest of the filtering at a later date. Would appreciate if @aaron-steinfeld can chime in on any more quick wins we can do here in the meantime though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay. approve this PR :)

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 buildGraphQlFilters(filters: FieldFilter[]): GraphQlFilter[] {
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(
Expand Down Expand Up @@ -37,9 +60,47 @@ export const toGraphQlOperator = (operator: FilterOperator): GraphQlOperatorType
return GraphQlOperatorType.Like;
case FilterOperator.In:
return GraphQlOperatorType.In;
case FilterOperator.NotIn:
return GraphQlOperatorType.NotIn;
case FilterOperator.ContainsKey:
return GraphQlOperatorType.ContainsKey;
default:
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:
return FilterOperator.NotIn;

case GraphQlOperatorType.ContainsKey:
return FilterOperator.ContainsKey;
default:
return assertUnreachable(operator);
}
};