Skip to content

Commit c01e0bc

Browse files
authored
Changes custom modal to be created dynamically (#6799)
## Motivation for features / changes The current Custom Modal can't display modals outside of their ancestors' stacking context (e.g. in scalar card tables: #6737 (comment) ), which significantly hinders scalar table context menu functionality. It also has some minor tricky issues that are difficult to fix like some modals lingering when another one is opened: ![image](https://github.com/tensorflow/tensorboard/assets/736199/934b1d0a-5650-47e2-94f4-e8836c1a6ab4) ## Technical description of changes - Before: Custom modals were defined in a consumer component's template. The modals were embedded views of the consumer component (e.g. DataTableComponent), which prevented them from being displayed outside the table's stacking context, e.g. outside of a scalar card table - After: Custom modals are still defined in a consumer component's template, but wrapped in ng-templates. They are dynamically created using a newly defined CustomModal service. When the appropriate CustomModal service method is a called, the modal template is attached as an embedded view using CDK Overlay, which places it in a top-level overlay container, freeing it from all stacking context issues. CustomModalComponent is completely subsumed by Overlay and is thus deleted. Only the CustomModal service will be required going forward. ## Detailed steps to verify changes work correctly (as executed by you) Manually tested all custom modal features in runs table, filter bar, scalar card table (Need to set query param `enableScalarColumnContextMenus=true` to test scalar table context menu behavior)
1 parent 71952d6 commit c01e0bc

15 files changed

+592
-434
lines changed

tensorboard/webapp/angular/BUILD

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,15 @@ tf_ts_library(
364364
],
365365
)
366366

367+
# This is a dummy rule used as a @angular/cdk/portal dependency.
368+
tf_ts_library(
369+
name = "expect_angular_cdk_portal",
370+
srcs = [],
371+
deps = [
372+
"@npm//@angular/cdk",
373+
],
374+
)
375+
367376
# This is a dummy rule used as a @angular/cdk/scrolling dependency.
368377
tf_ts_library(
369378
name = "expect_angular_cdk_scrolling",

tensorboard/webapp/runs/views/runs_table/filterbar_component.ng.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@
3131
</mat-chip-set>
3232
</div>
3333

34-
<custom-modal #filterModal (onClose)="deselectFilter()">
34+
<ng-template #filterModalTemplate>
3535
<tb-data-table-filter
36-
*ngIf="selectedFilter"
3736
[filter]="selectedFilter"
3837
(intervalFilterChanged)="emitIntervalFilterChanged($event)"
3938
(discreteFilterChanged)="emitDiscreteFilterChanged($event)"
4039
(includeUndefinedToggled)="emitIncludeUndefinedToggled()"
4140
></tb-data-table-filter>
42-
</custom-modal>
41+
</ng-template>

tensorboard/webapp/runs/views/runs_table/filterbar_component.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
EventEmitter,
2020
Component,
2121
ViewChild,
22+
TemplateRef,
23+
ViewContainerRef,
2224
} from '@angular/core';
2325
import {
2426
DiscreteFilter,
@@ -27,7 +29,7 @@ import {
2729
FilterAddedEvent,
2830
} from '../../../widgets/data_table/types';
2931
import {RangeValues} from '../../../widgets/range_input/types';
30-
import {CustomModalComponent} from '../../../widgets/custom_modal/custom_modal_component';
32+
import {CustomModal} from '../../../widgets/custom_modal/custom_modal';
3133

3234
@Component({
3335
selector: 'filterbar-component',
@@ -41,8 +43,8 @@ export class FilterbarComponent {
4143
@Output() removeHparamFilter = new EventEmitter<string>();
4244
@Output() addFilter = new EventEmitter<FilterAddedEvent>();
4345

44-
@ViewChild('filterModal', {static: false})
45-
private readonly filterModal!: CustomModalComponent;
46+
@ViewChild('filterModalTemplate', {read: TemplateRef})
47+
filterModalTemplate!: TemplateRef<unknown>;
4648

4749
private internalSelectedFilterName = '';
4850
get selectedFilterName(): string {
@@ -56,19 +58,18 @@ export class FilterbarComponent {
5658
return this.filters.get(this.selectedFilterName);
5759
}
5860

61+
constructor(
62+
private readonly customModal: CustomModal,
63+
private readonly viewContainerRef: ViewContainerRef
64+
) {}
65+
5966
openFilterMenu(event: MouseEvent, filterName: string) {
6067
this.selectedFilterName = filterName;
61-
const rect = (
62-
(event.target as HTMLElement).closest('mat-chip') as HTMLElement
63-
).getBoundingClientRect();
64-
this.filterModal.openAtPosition({
65-
x: rect.x + rect.width,
66-
y: rect.y,
67-
});
68-
}
69-
70-
deselectFilter() {
71-
this.selectedFilterName = '';
68+
this.customModal.createNextToElement(
69+
this.filterModalTemplate,
70+
(event.target as HTMLElement).closest('mat-chip') as HTMLElement,
71+
this.viewContainerRef
72+
);
7273
}
7374

7475
emitIntervalFilterChanged(value: RangeValues) {

tensorboard/webapp/runs/views/runs_table/filterbar_test.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
1515
import {ComponentFixture, TestBed} from '@angular/core/testing';
16-
import {NO_ERRORS_SCHEMA} from '@angular/core';
16+
import {Component, NO_ERRORS_SCHEMA} from '@angular/core';
1717
import {FilterbarComponent} from './filterbar_component';
1818
import {FilterbarContainer} from './filterbar_container';
1919
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -22,7 +22,6 @@ import {MockStore} from '@ngrx/store/testing';
2222
import {State} from '../../../app_state';
2323
import {Action, Store} from '@ngrx/store';
2424
import {By} from '@angular/platform-browser';
25-
import {CustomModalModule} from '../../../widgets/custom_modal/custom_modal_module';
2625
import {
2726
actions as hparamsActions,
2827
selectors as hparamsSelectors,
@@ -36,9 +35,9 @@ import {MatChipHarness} from '@angular/material/chips/testing';
3635
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
3736
import {MatChipRemove, MatChipsModule} from '@angular/material/chips';
3837
import {MatIconTestingModule} from '../../../testing/mat_icon_module';
39-
import {CustomModalComponent} from '../../../widgets/custom_modal/custom_modal_component';
4038
import {FilterDialogModule} from '../../../widgets/data_table/filter_dialog_module';
4139
import {FilterDialog} from '../../../widgets/data_table/filter_dialog_component';
40+
import {CustomModal} from '../../../widgets/custom_modal/custom_modal';
4241

4342
const discreteFilter: DiscreteFilter = {
4443
type: DomainType.DISCRETE,
@@ -61,6 +60,14 @@ const fakeFilterMap = new Map<string, DiscreteFilter | IntervalFilter>([
6160
['filter2', intervalFilter],
6261
]);
6362

63+
@Component({
64+
selector: 'testable-component',
65+
template: ` <filterbar></filterbar> `,
66+
})
67+
class TestableComponent {
68+
constructor(readonly customModal: CustomModal) {}
69+
}
70+
6471
describe('hparam_filterbar', () => {
6572
let actualActions: Action[];
6673
let store: MockStore<State>;
@@ -69,13 +76,12 @@ describe('hparam_filterbar', () => {
6976
beforeEach(async () => {
7077
await TestBed.configureTestingModule({
7178
imports: [
72-
CustomModalModule,
7379
NoopAnimationsModule,
7480
MatChipsModule,
7581
MatIconTestingModule,
7682
FilterDialogModule,
7783
],
78-
declarations: [FilterbarComponent, FilterbarContainer],
84+
declarations: [FilterbarComponent, FilterbarContainer, TestableComponent],
7985
providers: [provideMockTbStore()],
8086
schemas: [NO_ERRORS_SCHEMA],
8187
}).compileComponents();
@@ -85,23 +91,26 @@ describe('hparam_filterbar', () => {
8591
store?.resetSelectors();
8692
});
8793

88-
function createComponent(): ComponentFixture<FilterbarContainer> {
94+
function createComponent(): ComponentFixture<TestableComponent> {
8995
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
9096
actualActions = [];
9197
dispatchSpy = spyOn(store, 'dispatch').and.callFake((action: Action) => {
9298
actualActions.push(action);
9399
});
94100

95-
return TestBed.createComponent(FilterbarContainer);
101+
const fixture = TestBed.createComponent(TestableComponent);
102+
return fixture;
96103
}
97104

98105
it('renders hparam filterbar', () => {
99106
const fixture = createComponent();
100107
fixture.detectChanges();
101108

102-
const dialog = fixture.debugElement.query(By.directive(FilterbarComponent));
109+
const filterBarComponent = fixture.debugElement.query(
110+
By.directive(FilterbarComponent)
111+
);
103112

104-
expect(dialog).toBeTruthy();
113+
expect(filterBarComponent).toBeTruthy();
105114
});
106115

107116
it("doesn't render if no filters are set", async () => {
@@ -164,23 +173,23 @@ describe('hparam_filterbar', () => {
164173
const component = fixture.debugElement.query(
165174
By.directive(FilterbarComponent)
166175
).componentInstance;
167-
const openAtPositionSpy = spyOn(
168-
CustomModalComponent.prototype,
169-
'openAtPosition'
176+
const createNextToElementSpy = spyOn(
177+
TestBed.inject(CustomModal),
178+
'createNextToElement'
170179
);
171180
const loader = TestbedHarnessEnvironment.loader(fixture);
172181
fixture.detectChanges();
173182

174183
const chipHarness = await loader.getHarness(MatChipHarness);
175184
const chip = await chipHarness.host();
176-
const chipDimensions = await chip.getDimensions();
177185
await chip.click();
178186
fixture.detectChanges();
179187

180-
expect(openAtPositionSpy).toHaveBeenCalledWith({
181-
x: chipDimensions.left + chipDimensions.width,
182-
y: chipDimensions.top,
183-
});
188+
expect(createNextToElementSpy).toHaveBeenCalledWith(
189+
component.filterModalTemplate,
190+
fixture.debugElement.query(By.css('mat-chip')).nativeElement,
191+
component.viewContainerRef
192+
);
184193
expect(component.selectedFilterName).toBe('filter1');
185194
});
186195

tensorboard/webapp/runs/views/runs_table/runs_table_module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import {FilterbarComponent} from './filterbar_component';
4343
import {FilterbarContainer} from './filterbar_container';
4444
import {RunsGroupMenuButtonComponent} from './runs_group_menu_button_component';
4545
import {RunsGroupMenuButtonContainer} from './runs_group_menu_button_container';
46-
import {CustomModalModule} from '../../../widgets/custom_modal/custom_modal_module';
4746
import {RunsDataTable} from './runs_data_table';
4847
import {RunsTableContainer} from './runs_table_container';
4948

@@ -68,7 +67,6 @@ import {RunsTableContainer} from './runs_table_container';
6867
MatSortModule,
6968
MatTableModule,
7069
RangeInputModule,
71-
CustomModalModule,
7270
AlertModule,
7371
],
7472
exports: [RunsTableContainer],

tensorboard/webapp/widgets/custom_modal/BUILD

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ package(default_visibility = ["//tensorboard:internal"])
55
tf_ng_module(
66
name = "custom_modal",
77
srcs = [
8-
"custom_modal_component.ts",
9-
"custom_modal_module.ts",
8+
"custom_modal.ts",
109
],
1110
deps = [
12-
"@npm//@angular/common",
11+
"//tensorboard/webapp/angular:expect_angular_cdk_overlay",
12+
"//tensorboard/webapp/angular:expect_angular_cdk_portal",
13+
"//tensorboard/webapp/util:dom",
1314
"@npm//@angular/core",
1415
"@npm//rxjs",
1516
],
@@ -23,10 +24,12 @@ tf_ts_library(
2324
],
2425
deps = [
2526
":custom_modal",
27+
"//tensorboard/webapp/angular:expect_angular_cdk_overlay",
2628
"//tensorboard/webapp/angular:expect_angular_core_testing",
2729
"@npm//@angular/common",
2830
"@npm//@angular/core",
2931
"@npm//@angular/platform-browser",
3032
"@npm//@types/jasmine",
33+
"@npm//rxjs",
3134
],
3235
)

0 commit comments

Comments
 (0)