diff --git a/tensorboard/webapp/hparams/BUILD b/tensorboard/webapp/hparams/BUILD
index c3f0b23f58..0e7d632a7b 100644
--- a/tensorboard/webapp/hparams/BUILD
+++ b/tensorboard/webapp/hparams/BUILD
@@ -23,6 +23,7 @@ tf_ts_library(
deps = [
"//tensorboard/webapp/runs/data_source",
"//tensorboard/webapp/runs/data_source:backend_types",
+ "//tensorboard/webapp/widgets/data_table:types",
],
)
diff --git a/tensorboard/webapp/hparams/_types.ts b/tensorboard/webapp/hparams/_types.ts
index ccde5e297c..01df0df138 100644
--- a/tensorboard/webapp/hparams/_types.ts
+++ b/tensorboard/webapp/hparams/_types.ts
@@ -13,14 +13,12 @@ See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
import {
- HparamValue,
- MetricValue,
- DiscreteHparamValues,
- DomainType,
HparamSpec,
MetricSpec,
} from '../runs/data_source/runs_data_source_types';
+export {DiscreteFilter, IntervalFilter} from '../widgets/data_table/types';
+
export {
DatasetType,
DiscreteHparamValue,
@@ -50,21 +48,3 @@ export interface HparamAndMetricSpec {
hparams: HparamSpec[];
metrics: MetricSpec[];
}
-
-export interface DiscreteFilter {
- type: DomainType.DISCRETE;
- includeUndefined: boolean;
- possibleValues: DiscreteHparamValues;
- // Subset of `possibleValues`
- filterValues: DiscreteHparamValues;
-}
-
-export interface IntervalFilter {
- type: DomainType.INTERVAL;
- includeUndefined: boolean;
- minValue: number;
- maxValue: number;
- // Filter values have to be in between min and max values (inclusive).
- filterLowerValue: number;
- filterUpperValue: number;
-}
diff --git a/tensorboard/webapp/runs/data_source/BUILD b/tensorboard/webapp/runs/data_source/BUILD
index b93da6a6b4..fa8c2caf48 100644
--- a/tensorboard/webapp/runs/data_source/BUILD
+++ b/tensorboard/webapp/runs/data_source/BUILD
@@ -14,6 +14,7 @@ tf_ng_module(
deps = [
":backend_types",
"//tensorboard/webapp/webapp_data_source:http_client",
+ "//tensorboard/webapp/widgets/data_table:types",
"@npm//@angular/core",
"@npm//rxjs",
],
diff --git a/tensorboard/webapp/runs/data_source/runs_data_source.ts b/tensorboard/webapp/runs/data_source/runs_data_source.ts
index 23ee022c2c..b613638946 100644
--- a/tensorboard/webapp/runs/data_source/runs_data_source.ts
+++ b/tensorboard/webapp/runs/data_source/runs_data_source.ts
@@ -20,9 +20,9 @@ import {
TBHttpClient,
} from '../../webapp_data_source/tb_http_client';
import * as backendTypes from './runs_backend_types';
+import {DomainType} from '../../widgets/data_table/types';
import {
Domain,
- DomainType,
HparamsAndMetadata,
HparamSpec,
HparamValue,
diff --git a/tensorboard/webapp/runs/data_source/runs_data_source_types.ts b/tensorboard/webapp/runs/data_source/runs_data_source_types.ts
index fa33f26cc4..ac454f7ac1 100644
--- a/tensorboard/webapp/runs/data_source/runs_data_source_types.ts
+++ b/tensorboard/webapp/runs/data_source/runs_data_source_types.ts
@@ -16,6 +16,9 @@ import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import * as backendTypes from './runs_backend_types';
+import {DomainType} from '../../widgets/data_table/types';
+export {DomainType} from '../../widgets/data_table/types';
+
export {
BackendHparamsValueType as HparamsValueType,
DatasetType,
@@ -41,11 +44,6 @@ export interface RunToHparamsAndMetrics {
};
}
-export enum DomainType {
- DISCRETE,
- INTERVAL,
-}
-
interface IntervalDomain {
type: DomainType.INTERVAL;
minValue: number;
diff --git a/tensorboard/webapp/widgets/data_table/BUILD b/tensorboard/webapp/widgets/data_table/BUILD
index df4c373cc0..eed2d32dfb 100644
--- a/tensorboard/webapp/widgets/data_table/BUILD
+++ b/tensorboard/webapp/widgets/data_table/BUILD
@@ -51,6 +51,16 @@ tf_sass_binary(
],
)
+tf_sass_binary(
+ name = "filter_dialog_styles",
+ src = "filter_dialog_component.scss",
+ strict_deps = False,
+ deps = [
+ "//tensorboard/webapp:angular_material_sass_deps",
+ "//tensorboard/webapp/theme",
+ ],
+)
+
tf_ng_module(
name = "data_table",
srcs = [
@@ -71,12 +81,14 @@ tf_ng_module(
deps = [
":column_selector",
":data_table_header",
+ ":filter_dialog",
":types",
"//tensorboard/webapp/angular:expect_angular_material_button",
"//tensorboard/webapp/angular:expect_angular_material_icon",
"//tensorboard/webapp/metrics/views/card_renderer:scalar_card_types",
"//tensorboard/webapp/widgets/custom_modal",
"//tensorboard/webapp/widgets/line_chart_v2/lib:formatter",
+ "//tensorboard/webapp/widgets/range_input:types",
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//rxjs",
@@ -123,6 +135,27 @@ tf_ng_module(
],
)
+tf_ng_module(
+ name = "filter_dialog",
+ srcs = [
+ "filter_dialog_component.ts",
+ "filter_dialog_module.ts",
+ ],
+ assets = [
+ "filter_dialog_component.ng.html",
+ ":filter_dialog_styles",
+ ],
+ deps = [
+ ":types",
+ "//tensorboard/webapp/angular:expect_angular_material_checkbox",
+ "//tensorboard/webapp/widgets/filter_input",
+ "//tensorboard/webapp/widgets/range_input",
+ "//tensorboard/webapp/widgets/range_input:types",
+ "@npm//@angular/common",
+ "@npm//@angular/core",
+ ],
+)
+
tf_ts_library(
name = "types",
srcs = [
@@ -137,16 +170,23 @@ tf_ts_library(
"column_selector_test.ts",
"content_cell_component_test.ts",
"data_table_test.ts",
+ "filter_dialog_test.ts",
"header_cell_component_test.ts",
],
deps = [
":column_selector",
":data_table",
+ ":filter_dialog",
":types",
+ "//tensorboard/webapp/angular:expect_angular_cdk_testing",
+ "//tensorboard/webapp/angular:expect_angular_cdk_testing_testbed",
"//tensorboard/webapp/angular:expect_angular_core_testing",
+ "//tensorboard/webapp/angular:expect_angular_material_checkbox",
"//tensorboard/webapp/angular:expect_angular_platform_browser_animations",
"//tensorboard/webapp/testing:mat_icon",
"//tensorboard/webapp/widgets/custom_modal",
+ "//tensorboard/webapp/widgets/range_input",
+ "//tensorboard/webapp/widgets/range_input:types",
"@npm//@angular/core",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
diff --git a/tensorboard/webapp/widgets/data_table/filter_dialog_component.ng.html b/tensorboard/webapp/widgets/data_table/filter_dialog_component.ng.html
new file mode 100644
index 0000000000..cc5736e536
--- /dev/null
+++ b/tensorboard/webapp/widgets/data_table/filter_dialog_component.ng.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ No Matching Values
+
+
+ {{ value }}
+
+
+
+
+
+
+
+
Include Undefined
+
diff --git a/tensorboard/webapp/widgets/data_table/filter_dialog_component.scss b/tensorboard/webapp/widgets/data_table/filter_dialog_component.scss
new file mode 100644
index 0000000000..e6fb575d81
--- /dev/null
+++ b/tensorboard/webapp/widgets/data_table/filter_dialog_component.scss
@@ -0,0 +1,44 @@
+/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+@use '@angular/material' as mat;
+@import 'tensorboard/webapp/theme/tb_theme';
+
+.filter-dialog {
+ padding: 8px;
+ border-radius: 4px;
+ border: 1px solid;
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ border-color: mat.get-color-from-palette($tb-foreground, border);
+ background-color: mat.get-color-from-palette($tb-background, background);
+
+ @include tb-dark-theme {
+ border-color: mat.get-color-from-palette($tb-dark-foreground, border);
+ background-color: mat.get-color-from-palette(
+ $tb-dark-background,
+ 'background'
+ );
+ }
+
+ .discrete-filters-area {
+ max-height: 100px;
+ overflow-y: auto;
+ }
+
+ .no-matches {
+ // 12px is the width of the checkbox so the text should match the
+ // indentation of the selectable filters.
+ padding: 8px 12px;
+ }
+}
diff --git a/tensorboard/webapp/widgets/data_table/filter_dialog_component.ts b/tensorboard/webapp/widgets/data_table/filter_dialog_component.ts
new file mode 100644
index 0000000000..aab4c42340
--- /dev/null
+++ b/tensorboard/webapp/widgets/data_table/filter_dialog_component.ts
@@ -0,0 +1,56 @@
+/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {
+ DomainType,
+ DiscreteFilter,
+ IntervalFilter,
+ DiscreteFilterValue,
+} from './types';
+import {RangeValues} from '../range_input/types';
+
+@Component({
+ selector: 'tb-data-table-filter',
+ templateUrl: 'filter_dialog_component.ng.html',
+ styleUrls: ['filter_dialog_component.css'],
+})
+export class FilterDialog {
+ DomainType = DomainType;
+
+ discreteValueFilter: string = '';
+
+ @Input() filter!: DiscreteFilter | IntervalFilter;
+
+ @Output() discreteFilterChanged = new EventEmitter();
+
+ @Output() intervalFilterChanged = new EventEmitter();
+
+ @Output() includeUndefinedToggled = new EventEmitter();
+
+ getPossibleValues() {
+ const values: DiscreteFilterValue[] =
+ (this.filter as DiscreteFilter).possibleValues ?? [];
+ if (!this.discreteValueFilter) {
+ return values;
+ }
+ return values.filter((value) =>
+ value.toString().match(this.discreteValueFilter)
+ );
+ }
+
+ discreteValueKeyUp(event: KeyboardEvent) {
+ this.discreteValueFilter = (event.target! as HTMLInputElement).value;
+ }
+}
diff --git a/tensorboard/webapp/widgets/data_table/filter_dialog_module.ts b/tensorboard/webapp/widgets/data_table/filter_dialog_module.ts
new file mode 100644
index 0000000000..6cbc9a40f3
--- /dev/null
+++ b/tensorboard/webapp/widgets/data_table/filter_dialog_module.ts
@@ -0,0 +1,33 @@
+/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+import {NgModule} from '@angular/core';
+import {FilterDialog} from './filter_dialog_component';
+import {CommonModule} from '@angular/common';
+import {MatCheckboxModule} from '@angular/material/checkbox';
+import {RangeInputModule} from '../range_input/range_input_module';
+import {FilterInputModule} from '../filter_input/filter_input_module';
+
+@NgModule({
+ declarations: [FilterDialog],
+ imports: [
+ CommonModule,
+ MatCheckboxModule,
+ FilterInputModule,
+ RangeInputModule,
+ ],
+ exports: [FilterDialog],
+})
+export class FilterDialogModule {}
diff --git a/tensorboard/webapp/widgets/data_table/filter_dialog_test.ts b/tensorboard/webapp/widgets/data_table/filter_dialog_test.ts
new file mode 100644
index 0000000000..2930d90734
--- /dev/null
+++ b/tensorboard/webapp/widgets/data_table/filter_dialog_test.ts
@@ -0,0 +1,266 @@
+/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+import {TestBed} from '@angular/core/testing';
+import {HarnessLoader} from '@angular/cdk/testing';
+import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
+import {DataTableModule} from './data_table_module';
+import {FilterDialog} from './filter_dialog_component';
+import {
+ DiscreteFilter,
+ DomainType,
+ IntervalFilter,
+ DiscreteFilterValue,
+} from './types';
+import {By} from '@angular/platform-browser';
+import {RangeInputComponent} from '../range_input/range_input_component';
+import {RangeInputModule} from '../range_input/range_input_module';
+import {MatCheckboxModule} from '@angular/material/checkbox';
+import {MatCheckboxHarness} from '@angular/material/checkbox/testing';
+import {RangeInputSource} from '../range_input/types';
+
+describe('filter dialog', () => {
+ let rootLoader: HarnessLoader;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [FilterDialog],
+ imports: [DataTableModule, RangeInputModule, MatCheckboxModule],
+ }).compileComponents();
+ });
+
+ function createComponent(input: {filter: DiscreteFilter | IntervalFilter}) {
+ const fixture = TestBed.createComponent(FilterDialog);
+ fixture.componentInstance.filter = input.filter;
+ rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
+ fixture.detectChanges();
+ return fixture;
+ }
+
+ it('renders interval filters', () => {
+ const fixture = createComponent({
+ filter: {
+ type: DomainType.INTERVAL,
+ includeUndefined: false,
+ minValue: 6,
+ maxValue: 20,
+ filterLowerValue: 7,
+ filterUpperValue: 18,
+ },
+ });
+
+ const rangeInput = fixture.debugElement.query(
+ By.directive(RangeInputComponent)
+ );
+ expect(rangeInput).toBeTruthy();
+ const [lower, upper, ...rest] = rangeInput.queryAll(By.css('input'));
+ // There should only be two input fields.
+ expect(rest.length).toEqual(0);
+ expect(lower.nativeElement.value).toEqual('7');
+ expect(upper.nativeElement.value).toEqual('18');
+ });
+
+ it('dispatches event when an interval filter value is changed', async () => {
+ const fixture = createComponent({
+ filter: {
+ type: DomainType.INTERVAL,
+ includeUndefined: false,
+ minValue: 6,
+ maxValue: 20,
+ filterLowerValue: 7,
+ filterUpperValue: 18,
+ },
+ });
+
+ const intervalFilterChangedSpy = spyOn(
+ fixture.componentInstance.intervalFilterChanged,
+ 'emit'
+ );
+
+ const rangeInput = fixture.debugElement.query(
+ By.directive(RangeInputComponent)
+ );
+ rangeInput.componentInstance.rangeValuesChanged.emit({
+ lowerValue: 7,
+ upperValue: 17,
+ source: RangeInputSource.TEXT,
+ });
+ expect(intervalFilterChangedSpy).toHaveBeenCalledOnceWith({
+ lowerValue: 7,
+ upperValue: 17,
+ source: RangeInputSource.TEXT,
+ });
+ });
+
+ it('dispatches an event when include undefined is changed while viewing an interval filter', async () => {
+ const fixture = createComponent({
+ filter: {
+ type: DomainType.INTERVAL,
+ includeUndefined: false,
+ minValue: 6,
+ maxValue: 20,
+ filterLowerValue: 7,
+ filterUpperValue: 18,
+ },
+ });
+ const includeUndefinedSpy = spyOn(
+ fixture.componentInstance.includeUndefinedToggled,
+ 'emit'
+ );
+
+ const includeUndefinedCheckbox = await rootLoader.getHarness(
+ MatCheckboxHarness.with({label: 'Include Undefined'})
+ );
+ await includeUndefinedCheckbox.check();
+ expect(includeUndefinedSpy).toHaveBeenCalled();
+ });
+
+ it('renders discrete values', async () => {
+ createComponent({
+ filter: {
+ type: DomainType.DISCRETE,
+ includeUndefined: false,
+ possibleValues: [2, 4, 6, 8],
+ filterValues: [2, 4, 6],
+ },
+ });
+ const checkboxes = await rootLoader.getAllHarnesses(MatCheckboxHarness);
+
+ const checkboxLabels = await Promise.all(
+ checkboxes.map((checkbox) => checkbox.getLabelText())
+ );
+ expect(checkboxLabels).toEqual(['2', '4', '6', '8', 'Include Undefined']);
+ });
+
+ it('dispatches event when an discrete filter value is changed', async () => {
+ const possibleValues = [2, 4, 6, 8];
+ const fixture = createComponent({
+ filter: {
+ type: DomainType.DISCRETE,
+ includeUndefined: false,
+ possibleValues,
+ filterValues: [2, 4, 6],
+ },
+ });
+ const filterValues: DiscreteFilterValue[] = [];
+ spyOn(fixture.componentInstance.discreteFilterChanged, 'emit').and.callFake(
+ (value: DiscreteFilterValue) => filterValues.push(value)
+ );
+
+ for (const value of possibleValues) {
+ const checkbox = await rootLoader.getHarness(
+ MatCheckboxHarness.with({label: `${value}`})
+ );
+ await checkbox.uncheck();
+ }
+ expect(filterValues).toEqual([2, 4, 6]);
+
+ // Unchecking an unchecked box should not trigger an event.
+ for (const value of possibleValues) {
+ const checkbox = await rootLoader.getHarness(
+ MatCheckboxHarness.with({label: `${value}`})
+ );
+ await checkbox.uncheck();
+ }
+ expect(filterValues).toEqual([2, 4, 6]);
+
+ for (const value of possibleValues) {
+ const checkbox = await rootLoader.getHarness(
+ MatCheckboxHarness.with({label: `${value}`})
+ );
+ await checkbox.check();
+ }
+ expect(filterValues).toEqual([2, 4, 6, 2, 4, 6, 8]);
+
+ // Checking a checked box should not trigger an event.
+ for (const value of possibleValues) {
+ const checkbox = await rootLoader.getHarness(
+ MatCheckboxHarness.with({label: `${value}`})
+ );
+ await checkbox.check();
+ }
+ expect(filterValues).toEqual([2, 4, 6, 2, 4, 6, 8]);
+ });
+
+ it('dispatches an event when include undefined is changed while viewing a discrete filter', async () => {
+ const fixture = createComponent({
+ filter: {
+ type: DomainType.DISCRETE,
+ includeUndefined: false,
+ possibleValues: [2, 4, 6, 8],
+ filterValues: [2, 4, 6],
+ },
+ });
+ const includeUndefinedSpy = spyOn(
+ fixture.componentInstance.includeUndefinedToggled,
+ 'emit'
+ );
+
+ const includeUndefinedCheckbox = await rootLoader.getHarness(
+ MatCheckboxHarness.with({label: 'Include Undefined'})
+ );
+ await includeUndefinedCheckbox.check();
+ expect(includeUndefinedSpy).toHaveBeenCalled();
+ });
+
+ it('filters discrete values', async () => {
+ const fixture = createComponent({
+ filter: {
+ type: DomainType.DISCRETE,
+ includeUndefined: false,
+ possibleValues: ['foo', 'bar', 'baz', 'qaz'],
+ filterValues: ['foo', 'bar', 'baz', 'qaz'],
+ },
+ });
+ expect(await getCheckboxLabels()).toEqual([
+ 'foo',
+ 'bar',
+ 'baz',
+ 'qaz',
+ 'Include Undefined',
+ ]); // 4 options + the include undefined checkbox
+
+ fixture.componentInstance.discreteValueKeyUp(
+ // A mock of a keyboard input event.
+ {
+ target: {
+ value: 'ba',
+ },
+ } as any
+ );
+ fixture.detectChanges();
+ expect(await getCheckboxLabels()).toEqual([
+ 'bar',
+ 'baz',
+ 'Include Undefined',
+ ]); // 2 options + the include undefined checkbox
+
+ fixture.componentInstance.discreteValueKeyUp(
+ // A mock of a keyboard input event.
+ {
+ target: {
+ value: 'nothing matches me',
+ },
+ } as any
+ );
+ fixture.detectChanges();
+ expect(await getCheckboxLabels()).toEqual(['Include Undefined']); // 0 options + the include undefined checkbox
+ expect(fixture.nativeElement.innerHTML).toContain('No Matching Values');
+
+ async function getCheckboxLabels() {
+ const checkboxes = await rootLoader.getAllHarnesses(MatCheckboxHarness);
+ return Promise.all(checkboxes.map((checkbox) => checkbox.getLabelText()));
+ }
+ });
+});
diff --git a/tensorboard/webapp/widgets/data_table/types.ts b/tensorboard/webapp/widgets/data_table/types.ts
index 6e1a50d3cc..eca8c0e51a 100644
--- a/tensorboard/webapp/widgets/data_table/types.ts
+++ b/tensorboard/webapp/widgets/data_table/types.ts
@@ -12,7 +12,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
-
/**
* This enum defines the columns available in the data table. The
* ScalarCardComponent must know which piece of data is associated with each
@@ -23,6 +22,7 @@ export enum ColumnHeaderType {
RELATIVE_TIME = 'RELATIVE_TIME',
RUN = 'RUN',
STEP = 'STEP',
+ EXPERIMENT = 'EXPERIMENT',
TIME = 'TIME',
VALUE = 'VALUE',
SMOOTHED = 'SMOOTHED',
@@ -42,6 +42,33 @@ export enum ColumnHeaderType {
CUSTOM = 'CUSTOM',
}
+export enum DomainType {
+ DISCRETE,
+ INTERVAL,
+}
+
+export type DiscreteFilterValues = string[] | number[] | boolean[];
+
+export type DiscreteFilterValue = DiscreteFilterValues[number];
+
+export interface DiscreteFilter {
+ type: DomainType.DISCRETE;
+ includeUndefined: boolean;
+ possibleValues: DiscreteFilterValues;
+ // Subset of `possibleValues`
+ filterValues: DiscreteFilterValues;
+}
+
+export interface IntervalFilter {
+ type: DomainType.INTERVAL;
+ includeUndefined: boolean;
+ minValue: number;
+ maxValue: number;
+ // Filter values have to be in between min and max values (inclusive).
+ filterLowerValue: number;
+ filterUpperValue: number;
+}
+
export interface ColumnHeader {
type: ColumnHeaderType;
name: string;
@@ -52,6 +79,7 @@ export interface ColumnHeader {
removable?: boolean;
sortable?: boolean;
movable?: boolean;
+ filterable?: boolean;
}
export enum SortingOrder {