Skip to content

Commit f4b3436

Browse files
authored
Table multiselect filters (#823)
* feat: change table controls from select to multi-select dropdown filters * test: fix * feat: rename to add Table to beginning of API interfaces * feat: add validation to toInFilter * style: lint
1 parent 73bb82f commit f4b3436

File tree

11 files changed

+182
-151
lines changed

11 files changed

+182
-151
lines changed

projects/common/src/utilities/lang/lang-utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ const ignoreFunctions = (first: unknown, second: unknown) => {
3535
// tslint:disable-next-line: no-null-undefined-union
3636
export const isNonEmptyString = (str: string | undefined | null): str is string =>
3737
str !== undefined && str !== null && str !== '';
38+
39+
export const hasOwnProperty = <X extends {}, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown> =>
40+
// Since Typescript doesn't know how to type guard native hasOwnProperty, we wrap it here.
41+
obj.hasOwnProperty(prop);
Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,83 @@
11
import { Dictionary } from '@hypertrace/common';
2-
import { SelectOption } from '../../select/select-option';
2+
import { FilterOperator } from '../../filtering/filter/filter-operators';
33
import { TableFilter } from '../table-api';
44

5-
export interface SelectControl {
6-
placeholder?: string;
7-
default?: SelectOption<TableControlOption>;
8-
options: SelectOption<TableControlOption>[];
5+
export const enum TableControlOptionType {
6+
Filter = 'filter',
7+
Property = 'property',
8+
Unset = 'unset'
99
}
1010

11-
export interface SelectChange {
12-
select: SelectControl;
13-
value: TableControlOption;
14-
}
11+
export type TableControlOption = TableUnsetControlOption | TableFilterControlOption | TablePropertyControlOption;
1512

16-
export interface CheckboxControl {
13+
export interface TableFilterControlOption {
14+
type: TableControlOptionType.Filter;
1715
label: string;
18-
value: boolean;
19-
options: TableCheckboxOptions;
16+
metaValue: TableFilter;
2017
}
2118

22-
export interface CheckboxChange {
23-
checkbox: CheckboxControl;
24-
option: TableControlOption<boolean>;
19+
export interface TableUnsetControlOption {
20+
type: TableControlOptionType.Unset;
21+
label: string;
22+
metaValue: string;
2523
}
2624

27-
export const enum TableControlOptionType {
28-
Filter = 'filter',
29-
Property = 'property',
30-
UnsetFilter = 'unset-filter'
25+
export interface TablePropertyControlOption {
26+
type: TableControlOptionType.Property;
27+
label: string;
28+
metaValue: Dictionary<unknown>;
3129
}
3230

33-
export type TableControlOption<T = unknown> =
34-
| TableUnsetFilterControlOption<T>
35-
| TableFilterControlOption<T>
36-
| TablePropertyControlOption<T>;
31+
/*
32+
* Select Control
33+
*/
3734

38-
interface TableControlOptionBase<T> {
39-
value?: T;
35+
export interface TableSelectControl {
36+
placeholder: string;
37+
options: TableSelectControlOption[];
4038
}
41-
export interface TableUnsetFilterControlOption<T = unknown> extends TableControlOptionBase<T> {
42-
type: TableControlOptionType.UnsetFilter;
43-
metaValue: string;
39+
40+
export interface TableSelectChange {
41+
select: TableSelectControl;
42+
values: TableSelectControlOption[];
4443
}
4544

46-
export interface TableFilterControlOption<T = unknown> extends TableControlOptionBase<T> {
47-
type: TableControlOptionType.Filter;
48-
metaValue: TableFilter;
45+
export type TableSelectControlOption = TableFilterControlOption;
46+
47+
/*
48+
* Checkbox Control
49+
*/
50+
51+
export interface TableCheckboxControl {
52+
label: string;
53+
value: boolean;
54+
options: TableCheckboxOptions;
4955
}
5056

51-
export interface TablePropertyControlOption<T = unknown> extends TableControlOptionBase<T> {
52-
type: TableControlOptionType.Property;
53-
metaValue: Dictionary<unknown>;
57+
export interface TableCheckboxChange {
58+
checkbox: TableCheckboxControl;
59+
option: TableCheckboxControlOption;
5460
}
5561

56-
export type TableCheckboxControlOption<T extends boolean> = TableControlOption<T> & { label: string };
62+
export type TableCheckboxControlOption<T = boolean> = TableControlOption & {
63+
value: T;
64+
};
5765

5866
export type TableCheckboxOptions = [TableCheckboxControlOption<true>, TableCheckboxControlOption<false>];
67+
68+
/*
69+
* Util
70+
*/
71+
72+
export const toInFilter = (tableFilters: TableFilter[]): TableFilter =>
73+
tableFilters.reduce((previousValue, currentValue) => {
74+
if (currentValue.operator !== FilterOperator.Equals || previousValue.field !== currentValue.field) {
75+
throw Error('Filters must all contain same field and use = operator');
76+
}
77+
78+
return {
79+
field: previousValue.field,
80+
operator: FilterOperator.In,
81+
value: [...(Array.isArray(previousValue.value) ? previousValue.value : [previousValue.value]), currentValue.value]
82+
};
83+
});

projects/components/src/table/controls/table-controls.component.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fakeAsync } from '@angular/core/testing';
22
import { SubscriptionLifecycle } from '@hypertrace/common';
3-
import { SelectComponent } from '@hypertrace/components';
3+
import { MultiSelectComponent } from '@hypertrace/components';
44
import { createHostFactory, mockProvider } from '@ngneat/spectator/jest';
55
import { MockComponent } from 'ng-mocks';
66
import { SearchBoxComponent } from '../../search-box/search-box.component';
@@ -14,7 +14,7 @@ describe('Table Controls component', () => {
1414
providers: [mockProvider(SubscriptionLifecycle)],
1515
declarations: [
1616
MockComponent(SearchBoxComponent),
17-
MockComponent(SelectComponent),
17+
MockComponent(MultiSelectComponent),
1818
MockComponent(ToggleGroupComponent)
1919
],
2020
template: `
@@ -67,19 +67,19 @@ describe('Table Controls component', () => {
6767
options: [
6868
{
6969
label: 'first1',
70-
value: 'first-1'
70+
metaValue: 'first-1'
7171
},
7272
{
7373
label: 'second1',
74-
value: 'second-1'
74+
metaValue: 'second-1'
7575
}
7676
]
7777
}
7878
]
7979
}
8080
});
8181

82-
expect(spectator.query(SelectComponent)?.placeholder).toEqual('test1');
82+
expect(spectator.query(MultiSelectComponent)?.placeholder).toEqual('test1');
8383
});
8484

8585
test('should emit selection when selected', () => {
@@ -106,10 +106,12 @@ describe('Table Controls component', () => {
106106
}
107107
});
108108

109-
spectator.triggerEventHandler(SelectComponent, 'selectedChange', {
110-
label: 'first1',
111-
value: 'first-1'
112-
});
109+
spectator.triggerEventHandler(MultiSelectComponent, 'selectedChange', [
110+
{
111+
label: 'first1',
112+
value: 'first-1'
113+
}
114+
]);
113115

114116
expect(onChangeSpy).toHaveBeenCalled();
115117
});

projects/components/src/table/controls/table-controls.component.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@ import { Subject } from 'rxjs';
1414
import { debounceTime } from 'rxjs/operators';
1515
import { IconSize } from '../../icon/icon-size';
1616
import { MultiSelectJustify } from '../../multi-select/multi-select-justify';
17-
import { TriggerLabelDisplayMode } from '../../multi-select/multi-select.component';
17+
import { MultiSelectSearchMode, TriggerLabelDisplayMode } from '../../multi-select/multi-select.component';
1818
import { ToggleItem } from '../../toggle-group/toggle-item';
19-
import { CheckboxChange, CheckboxControl, SelectChange, SelectControl, TableControlOption } from './table-controls-api';
19+
import {
20+
TableCheckboxChange,
21+
TableCheckboxControl,
22+
TableSelectChange,
23+
TableSelectControl,
24+
TableSelectControlOption
25+
} from './table-controls-api';
2026

2127
@Component({
2228
selector: 'ht-table-controls',
@@ -36,19 +42,20 @@ import { CheckboxChange, CheckboxControl, SelectChange, SelectControl, TableCont
3642
></ht-search-box>
3743
3844
<!-- Selects -->
39-
<ht-select
45+
<ht-multi-select
4046
*ngFor="let selectControl of this.selectControls"
4147
[placeholder]="selectControl.placeholder"
42-
[selected]="selectControl.default?.value"
4348
class="control select"
44-
(selectedChange)="this.onSelectChange(selectControl, $event)"
49+
showBorder="true"
50+
searchMode="${MultiSelectSearchMode.CaseInsensitive}"
51+
(selectedChange)="this.onMultiSelectChange(selectControl, $event)"
4552
>
4653
<ht-select-option
4754
*ngFor="let option of selectControl.options"
4855
[label]="option.label"
49-
[value]="option.value"
56+
[value]="option"
5057
></ht-select-option>
51-
</ht-select>
58+
</ht-multi-select>
5259
</div>
5360
5461
<!-- Right -->
@@ -86,17 +93,18 @@ import { CheckboxChange, CheckboxControl, SelectChange, SelectControl, TableCont
8693
})
8794
export class TableControlsComponent implements OnChanges {
8895
public readonly DEFAULT_SEARCH_PLACEHOLDER: string = 'Search...';
96+
8997
@Input()
9098
public searchEnabled?: boolean;
9199

92100
@Input()
93101
public searchPlaceholder?: string;
94102

95103
@Input()
96-
public selectControls?: SelectControl[] = [];
104+
public selectControls?: TableSelectControl[] = [];
97105

98106
@Input()
99-
public checkboxControls?: CheckboxControl[] = [];
107+
public checkboxControls?: TableCheckboxControl[] = [];
100108

101109
@Input()
102110
public activeFilterItem?: ToggleItem;
@@ -111,10 +119,10 @@ export class TableControlsComponent implements OnChanges {
111119
public readonly searchChange: EventEmitter<string> = new EventEmitter<string>();
112120

113121
@Output()
114-
public readonly selectChange: EventEmitter<SelectChange> = new EventEmitter<SelectChange>();
122+
public readonly selectChange: EventEmitter<TableSelectChange> = new EventEmitter<TableSelectChange>();
115123

116124
@Output()
117-
public readonly checkboxChange: EventEmitter<CheckboxChange> = new EventEmitter<CheckboxChange>();
125+
public readonly checkboxChange: EventEmitter<TableCheckboxChange> = new EventEmitter<TableCheckboxChange>();
118126

119127
@Output()
120128
public readonly viewChange: EventEmitter<string> = new EventEmitter<string>();
@@ -179,21 +187,21 @@ export class TableControlsComponent implements OnChanges {
179187
this.searchDebounceSubject.next(text);
180188
}
181189

182-
public onSelectChange(select: SelectControl, value: TableControlOption): void {
190+
public onMultiSelectChange(select: TableSelectControl, selections: TableSelectControlOption[]): void {
183191
this.selectChange.emit({
184192
select: select,
185-
value: value
193+
values: selections
186194
});
187195
}
188196

189-
public onCheckboxChange(checks: string[]): void {
190-
const diff = this.checkboxDiffer?.diff(checks);
197+
public onCheckboxChange(checked: string[]): void {
198+
const diff = this.checkboxDiffer?.diff(checked);
191199
if (!diff) {
192200
return;
193201
}
194202

195203
diff.forEachAddedItem(addedItem => {
196-
const found: CheckboxControl | undefined = this.checkboxControls?.find(
204+
const found: TableCheckboxControl | undefined = this.checkboxControls?.find(
197205
control => control.label === addedItem.item
198206
);
199207
if (found) {
@@ -205,7 +213,7 @@ export class TableControlsComponent implements OnChanges {
205213
});
206214

207215
diff.forEachRemovedItem(removedItem => {
208-
const found: CheckboxControl | undefined = this.checkboxControls?.find(
216+
const found: TableCheckboxControl | undefined = this.checkboxControls?.find(
209217
control => control.label === removedItem.item
210218
);
211219
if (found) {
@@ -216,7 +224,7 @@ export class TableControlsComponent implements OnChanges {
216224
}
217225
});
218226

219-
this.checkboxSelections = checks;
227+
this.checkboxSelections = checked;
220228
this.checkboxDiffer?.diff(this.checkboxSelections);
221229
}
222230

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1+
import { TableControlOption } from '@hypertrace/components';
12
import { Observable } from 'rxjs';
2-
import { LabeledTableControlOption } from '../../widgets/table/table-widget-control.model';
33
import { GraphQlDataSourceModel } from './graphql-data-source.model';
44

5-
export abstract class GraphqlTableControlOptionsDataSourceModel extends GraphQlDataSourceModel<
6-
LabeledTableControlOption[]
7-
> {
8-
public abstract getData(): Observable<LabeledTableControlOption[]>;
5+
export abstract class GraphqlTableControlOptionsDataSourceModel extends GraphQlDataSourceModel<TableControlOption[]> {
6+
public abstract getData(): Observable<TableControlOption[]>;
97
}

projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-control-checkbox-option.model.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { TableCheckboxOptions, TableControlOption } from '@hypertrace/components';
1+
import { hasOwnProperty } from '@hypertrace/common';
2+
import { TableCheckboxControlOption, TableCheckboxOptions, TableControlOption } from '@hypertrace/components';
23
import { BOOLEAN_PROPERTY, Model, ModelProperty } from '@hypertrace/hyperdash';
34
import { Observable } from 'rxjs';
45
import { map } from 'rxjs/operators';
@@ -7,7 +8,7 @@ import { TableWidgetControlModel } from './table-widget-control.model';
78
@Model({
89
type: 'table-widget-checkbox-option'
910
})
10-
export class TableWidgetControlCheckboxOptionModel extends TableWidgetControlModel {
11+
export class TableWidgetControlCheckboxOptionModel extends TableWidgetControlModel<TableCheckboxControlOption> {
1112
@ModelProperty({
1213
key: 'checked',
1314
displayName: 'Checked',
@@ -19,7 +20,7 @@ export class TableWidgetControlCheckboxOptionModel extends TableWidgetControlMod
1920
public getOptions(): Observable<TableCheckboxOptions> {
2021
return super.getOptions().pipe(
2122
map(options => {
22-
if (!this.isValidCheckboxControlOption(options)) {
23+
if (!this.isValidCheckboxControlOptions(options)) {
2324
throw Error(`Invalid table widget checkbox data source for options '${JSON.stringify(options)}'`);
2425
}
2526

@@ -31,7 +32,10 @@ export class TableWidgetControlCheckboxOptionModel extends TableWidgetControlMod
3132
);
3233
}
3334

34-
private isValidCheckboxControlOption(options: TableControlOption[]): options is TableCheckboxOptions {
35-
return options.length === 2 && options.every(option => typeof option.value === 'boolean');
35+
private isValidCheckboxControlOptions(options: TableControlOption[]): options is TableCheckboxOptions {
36+
return (
37+
options.length === 2 &&
38+
options.every(option => hasOwnProperty(option, 'value') && typeof option.value === 'boolean')
39+
);
3640
}
3741
}
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
1+
import { TableControlOption, TableControlOptionType, TableSelectControlOption } from '@hypertrace/components';
12
import { Model, ModelProperty, STRING_PROPERTY } from '@hypertrace/hyperdash';
3+
import { Observable } from 'rxjs';
4+
import { map } from 'rxjs/operators';
25
import { TableWidgetControlModel } from './table-widget-control.model';
36

47
@Model({
58
type: 'table-widget-select-option'
69
})
7-
export class TableWidgetControlSelectOptionModel extends TableWidgetControlModel {
10+
export class TableWidgetControlSelectOptionModel extends TableWidgetControlModel<TableSelectControlOption> {
811
@ModelProperty({
912
key: 'placeholder',
1013
displayName: 'Placeholder',
11-
type: STRING_PROPERTY.type
14+
type: STRING_PROPERTY.type,
15+
required: true
1216
})
13-
public placeholder?: string;
17+
public placeholder!: string;
18+
19+
public getOptions(): Observable<TableSelectControlOption[]> {
20+
return super.getOptions().pipe(
21+
map(options => {
22+
if (!this.isValidSelectControlOptions(options)) {
23+
throw Error(`Invalid table widget select data source for options '${JSON.stringify(options)}'`);
24+
}
25+
26+
return options;
27+
})
28+
);
29+
}
30+
31+
private isValidSelectControlOptions(options: TableControlOption[]): options is TableSelectControlOption[] {
32+
return options.every(option => option.type === TableControlOptionType.Filter);
33+
}
1434
}

0 commit comments

Comments
 (0)