Skip to content

Commit b65bcb4

Browse files
authored
HParams: Create column selector component (#6412)
## Motivation for features / changes As part of the hparams in time series project we are creating a new tool for adding columns to tables. ## Technical description of changes ## Screenshots of UI changes (or N/A) Light Mode: ![image](https://github.com/tensorflow/tensorboard/assets/78179109/e276e60f-4f54-4002-ba23-a831fc749e17) Dark Mode: ![image](https://github.com/tensorflow/tensorboard/assets/78179109/85bd36f8-9c69-4c85-a6ea-1b836b4d2b32) Here's what it's like to use ![c76ad5f4-977d-4450-9dd0-b58e87b81df1](https://github.com/tensorflow/tensorboard/assets/78179109/e8fee638-30b7-47be-be1c-209cc9794ab4) ## Detailed steps to verify changes work correctly (as executed by you) ## Alternate designs / implementations considered (or N/A)
1 parent 39172a9 commit b65bcb4

File tree

6 files changed

+501
-0
lines changed

6 files changed

+501
-0
lines changed

tensorboard/webapp/widgets/data_table/BUILD

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ tf_sass_binary(
2222
],
2323
)
2424

25+
tf_sass_binary(
26+
name = "column_selector_component_styles",
27+
src = "column_selector_component.scss",
28+
strict_deps = False,
29+
deps = [
30+
"//tensorboard/webapp:angular_material_sass_deps",
31+
"//tensorboard/webapp/theme",
32+
],
33+
)
34+
2535
tf_ng_module(
2636
name = "data_table",
2737
srcs = [
@@ -61,6 +71,29 @@ tf_ng_module(
6171
],
6272
)
6373

74+
tf_ng_module(
75+
name = "column_selector",
76+
srcs = [
77+
"column_selector_component.ts",
78+
"column_selector_module.ts",
79+
],
80+
assets = [
81+
"column_selector_component.ng.html",
82+
":column_selector_component_styles",
83+
],
84+
deps = [
85+
":types",
86+
"//tensorboard/webapp/angular:expect_angular_material_button",
87+
"//tensorboard/webapp/angular:expect_angular_material_dialog",
88+
"//tensorboard/webapp/angular:expect_angular_material_icon",
89+
"//tensorboard/webapp/angular:expect_angular_material_input",
90+
"@npm//@angular/common",
91+
"@npm//@angular/core",
92+
"@npm//@angular/forms",
93+
"@npm//rxjs",
94+
],
95+
)
96+
6497
tf_ts_library(
6598
name = "types",
6699
srcs = [
@@ -72,14 +105,17 @@ tf_ts_library(
72105
name = "data_table_test",
73106
testonly = True,
74107
srcs = [
108+
"column_selector_test.ts",
75109
"data_table_test.ts",
76110
],
77111
deps = [
112+
":column_selector",
78113
":data_table",
79114
":types",
80115
"//tensorboard/webapp/angular:expect_angular_core_testing",
81116
"//tensorboard/webapp/angular:expect_angular_material_icon",
82117
"@npm//@angular/core",
118+
"@npm//@angular/forms",
83119
"@npm//@angular/platform-browser",
84120
"@npm//@types/jasmine",
85121
],
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!--
2+
@license
3+
Copyright 2023 The TensorFlow Authors. All Rights Reserved.
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
-->
14+
<div class="contents">
15+
<mat-form-field class="search-area">
16+
<mat-icon matPrefix class="search-icon" svgIcon="search_24px"></mat-icon>
17+
<input
18+
matInput
19+
#search
20+
[(ngModel)]="searchInput"
21+
placeholder="Search"
22+
(ngModelChange)="searchInputChanged()"
23+
/>
24+
</mat-form-field>
25+
26+
<div #columnList class="column-list">
27+
<button
28+
mat-button
29+
*ngFor="let column of getFilteredColumns(); let i = index"
30+
class="column-button"
31+
[ngClass]="{
32+
'selected': i === (selectedIndex$ | async)
33+
}"
34+
(click)="selectColumn(column)"
35+
>
36+
{{column.displayName}}
37+
</button>
38+
</div>
39+
</div>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
@use '@angular/material' as mat;
16+
@import 'tensorboard/webapp/theme/tb_theme';
17+
18+
.contents {
19+
display: flex;
20+
flex-direction: column;
21+
align-items: flex-start;
22+
padding: 8px;
23+
24+
border-radius: 4px;
25+
border: 1px solid;
26+
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
27+
28+
border-color: mat.get-color-from-palette($tb-foreground, border);
29+
background-color: mat.get-color-from-palette($tb-background, background);
30+
31+
@include tb-dark-theme {
32+
border-color: mat.get-color-from-palette($tb-dark-foreground, border);
33+
background-color: mat.get-color-from-palette(
34+
$tb-dark-background,
35+
'background'
36+
);
37+
}
38+
39+
.search-area {
40+
margin-bottom: 4px;
41+
42+
mat-icon.search-icon {
43+
font-size: 1em;
44+
45+
::ng-deep svg {
46+
padding-top: 6px;
47+
}
48+
}
49+
}
50+
51+
.column-list {
52+
display: flex;
53+
flex-direction: column;
54+
width: 100%;
55+
56+
max-height: 200px;
57+
overflow-y: scroll;
58+
}
59+
60+
.column-button {
61+
text-align: left;
62+
width: 100%;
63+
64+
&.selected {
65+
background-color: mat.get-color-from-palette(mat.$gray-palette, 200);
66+
67+
@include tb-dark-theme {
68+
background-color: mat.get-color-from-palette(mat.$gray-palette, 400);
69+
}
70+
}
71+
}
72+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {
16+
Component,
17+
EventEmitter,
18+
Output,
19+
Input,
20+
ViewChild,
21+
ElementRef,
22+
AfterViewInit,
23+
HostListener,
24+
OnInit,
25+
ChangeDetectionStrategy,
26+
} from '@angular/core';
27+
import {ColumnHeader} from './types';
28+
import {BehaviorSubject} from 'rxjs';
29+
30+
@Component({
31+
selector: 'tb-data-table-column-selector-component',
32+
templateUrl: 'column_selector_component.ng.html',
33+
styleUrls: ['column_selector_component.css'],
34+
changeDetection: ChangeDetectionStrategy.OnPush,
35+
})
36+
export class ColumnSelectorComponent implements OnInit, AfterViewInit {
37+
@Input() selectableColumns: ColumnHeader[] = [];
38+
@Output() columnSelected = new EventEmitter<ColumnHeader>();
39+
40+
@ViewChild('search')
41+
private readonly searchField!: ElementRef;
42+
43+
@ViewChild('columnList')
44+
private readonly columnList!: ElementRef;
45+
46+
searchInput = '';
47+
selectedIndex$ = new BehaviorSubject(0);
48+
49+
ngOnInit() {
50+
/**
51+
* This components supports keyboard navigation.
52+
* Pressing the up arrow selects the previous column.
53+
* Pressing the down arrow selects the next column.
54+
* Pressing the enter key adds the selected column.
55+
*
56+
* When the selected column is outside the visible area (due to scolling)
57+
* we update the scrollTop to ensure the visible area follows the selected
58+
* column.
59+
*/
60+
this.selectedIndex$.subscribe(() => {
61+
if (!this.columnList) {
62+
return;
63+
}
64+
const selectedButton: HTMLButtonElement =
65+
this.columnList.nativeElement.querySelector('button.selected');
66+
if (!selectedButton) return;
67+
68+
const scrollAreaHeight: number =
69+
this.columnList.nativeElement.getBoundingClientRect().height;
70+
const buttonHeight = selectedButton.getBoundingClientRect().height;
71+
const scrollTop = this.columnList.nativeElement.scrollTop;
72+
// If we need to scroll up.
73+
if (this.selectedIndex$.getValue() * buttonHeight < scrollTop) {
74+
this.columnList.nativeElement.scrollTop =
75+
this.selectedIndex$.getValue() * buttonHeight;
76+
}
77+
78+
// If we need to scroll down.
79+
if (
80+
(this.selectedIndex$.getValue() + 1) * buttonHeight >
81+
scrollTop + scrollAreaHeight
82+
) {
83+
this.columnList.nativeElement.scrollTop =
84+
(this.selectedIndex$.getValue() + 1) * buttonHeight -
85+
scrollAreaHeight;
86+
}
87+
});
88+
}
89+
90+
ngAfterViewInit() {
91+
this.searchInput = '';
92+
this.searchField.nativeElement.focus();
93+
this.selectedIndex$.next(0);
94+
}
95+
96+
focus() {
97+
this.searchField?.nativeElement.focus();
98+
}
99+
100+
getFilteredColumns() {
101+
return this.selectableColumns.filter(
102+
(columnHeader) =>
103+
columnHeader.name.toLowerCase().match(this.searchInput.toLowerCase()) ||
104+
columnHeader.displayName
105+
.toLowerCase()
106+
.match(this.searchInput.toLowerCase())
107+
);
108+
}
109+
110+
searchInputChanged() {
111+
this.selectedIndex$.next(
112+
Math.min(
113+
this.selectedIndex$.getValue(),
114+
this.selectableColumns.length - 1
115+
)
116+
);
117+
}
118+
119+
selectColumn(header: ColumnHeader) {
120+
this.selectedIndex$.next(0);
121+
this.columnSelected.emit(header);
122+
}
123+
124+
@HostListener('document:keydown.arrowup', ['$event'])
125+
onUpArrow() {
126+
this.selectedIndex$.next(Math.max(this.selectedIndex$.getValue() - 1, 0));
127+
}
128+
129+
@HostListener('document:keydown.arrowdown', ['$event'])
130+
onDownArrow() {
131+
this.selectedIndex$.next(
132+
Math.min(
133+
this.selectedIndex$.getValue() + 1,
134+
this.getFilteredColumns().length - 1
135+
)
136+
);
137+
}
138+
139+
@HostListener('document:keydown.enter', ['$event'])
140+
onEnterPressed() {
141+
this.selectColumn(
142+
this.getFilteredColumns()[this.selectedIndex$.getValue()]
143+
);
144+
}
145+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
16+
import {CommonModule} from '@angular/common';
17+
import {NgModule} from '@angular/core';
18+
import {MatIconModule} from '@angular/material/icon';
19+
import {MatInputModule} from '@angular/material/input';
20+
import {MatButtonModule} from '@angular/material/button';
21+
import {ColumnSelectorComponent} from './column_selector_component';
22+
import {FormsModule} from '@angular/forms';
23+
import {MatDialogModule} from '@angular/material/dialog';
24+
25+
@NgModule({
26+
declarations: [ColumnSelectorComponent],
27+
imports: [
28+
CommonModule,
29+
MatIconModule,
30+
MatInputModule,
31+
MatButtonModule,
32+
MatDialogModule,
33+
FormsModule,
34+
],
35+
exports: [ColumnSelectorComponent],
36+
})
37+
export class ColumnSelectorModule {}

0 commit comments

Comments
 (0)