Skip to content

Commit 15602e9

Browse files
authored
Show flags page as modal when showFlags query parameter is set (#5800)
* Add new feature flag to control showing the feature flag modal, add modal to wrapper component to show flags page when the showFlags query parameter is set
1 parent 6814996 commit 15602e9

19 files changed

+703
-48
lines changed

tensorboard/webapp/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ tf_ng_module(
120120
"//tensorboard/webapp/core/views:page_title",
121121
"//tensorboard/webapp/experiments",
122122
"//tensorboard/webapp/feature_flag",
123+
"//tensorboard/webapp/feature_flag/views:feature_flag_modal_trigger",
123124
"//tensorboard/webapp/header",
124125
"//tensorboard/webapp/hparams",
125126
"//tensorboard/webapp/persistent_settings",
@@ -265,7 +266,6 @@ tf_ng_web_test_suite(
265266
"//tensorboard/webapp/core/views:test_lib",
266267
"//tensorboard/webapp/customization:customization_test_lib",
267268
"//tensorboard/webapp/deeplink:deeplink_test_lib",
268-
"//tensorboard/webapp/feature_flag/effects:effects_test_lib",
269269
"//tensorboard/webapp/header:test_lib",
270270
"//tensorboard/webapp/metrics:integration_test",
271271
"//tensorboard/webapp/metrics:test_lib",

tensorboard/webapp/app_container.ng.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@
2424
<page-title></page-title>
2525
<settings-polymer-interop></settings-polymer-interop>
2626
<dark-mode-supporter></dark-mode-supporter>
27+
<feature-flag-modal-trigger></feature-flag-modal-trigger>

tensorboard/webapp/app_module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {HashStorageModule} from './core/views/hash_storage_module';
2828
import {PageTitleModule} from './core/views/page_title_module';
2929
import {ExperimentsModule} from './experiments/experiments_module';
3030
import {FeatureFlagModule} from './feature_flag/feature_flag_module';
31+
import {FeatureFlagModalTriggerModule} from './feature_flag/views/feature_flag_modal_trigger_module';
3132
import {HeaderModule} from './header/header_module';
3233
import {HparamsModule} from './hparams/hparams_module';
3334
import {MatIconModule} from './mat_icon_module';
@@ -45,6 +46,7 @@ import {TensorBoardWrapperModule} from './tb_wrapper/tb_wrapper_module';
4546
imports: [
4647
// Ensure feature flags are enabled before they are consumed.
4748
FeatureFlagModule,
49+
FeatureFlagModalTriggerModule,
4850
BrowserModule,
4951
BrowserAnimationsModule,
5052
AppRoutingModule,

tensorboard/webapp/feature_flag/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,6 @@ tf_ng_web_test_suite(
6464
"//tensorboard/webapp/feature_flag/effects:effects_test_lib",
6565
"//tensorboard/webapp/feature_flag/http:http_test_lib",
6666
"//tensorboard/webapp/feature_flag/store:store_test_lib",
67+
"//tensorboard/webapp/feature_flag/views:views_test",
6768
],
6869
)

tensorboard/webapp/feature_flag/store/feature_flag_metadata.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ export const FeatureFlagMetadataMap: FeatureFlagMetadataMapType<FeatureFlags> =
106106
defaultValue: true,
107107
queryParamOverride: null,
108108
},
109+
enableShowFlags: {
110+
defaultValue: false,
111+
queryParamOverride: 'showFlags',
112+
parseValue: parseBoolean,
113+
},
109114
};
110115

111116
/**

tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,10 @@ export const getIsDataTableEnabled = createSelector(
149149
return flags.enabledScalarDataTable;
150150
}
151151
);
152+
153+
export const getShowFlagsEnabled = createSelector(
154+
getFeatureFlags,
155+
(flags: FeatureFlags): boolean => {
156+
return flags.enableShowFlags;
157+
}
158+
);

tensorboard/webapp/feature_flag/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ export interface FeatureFlags {
4444
forceSvg: boolean;
4545
// Whether to enable the "sticky" data table in scalar cards.
4646
enabledScalarDataTable: boolean;
47+
// If enabled causes the feature flags modal to appear.
48+
enableShowFlags: boolean;
4749
}
Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,85 @@
1-
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_sass_binary")
1+
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_sass_binary", "tf_ts_library")
22

33
package(default_visibility = ["//tensorboard:internal"])
44

55
licenses(["notice"])
66

77
tf_sass_binary(
88
name = "style",
9-
src = "feature_flag_page_style.scss",
9+
src = "feature_flag_page_component.scss",
1010
deps = ["//tensorboard/webapp:angular_material_sass_deps"],
1111
)
1212

13+
tf_ng_module(
14+
name = "feature_flag_modal_trigger",
15+
srcs = [
16+
"feature_flag_modal_trigger_container.ts",
17+
"feature_flag_modal_trigger_module.ts",
18+
],
19+
deps = [
20+
":views",
21+
"//tensorboard/webapp:app_state",
22+
"//tensorboard/webapp/angular:expect_angular_material_checkbox",
23+
"//tensorboard/webapp/feature_flag/actions",
24+
"//tensorboard/webapp/feature_flag/store",
25+
"@npm//@angular/common",
26+
"@npm//@angular/core",
27+
"@npm//@ngrx/store",
28+
"@npm//rxjs",
29+
],
30+
)
31+
1332
tf_ng_module(
1433
name = "views",
1534
srcs = [
1635
"feature_flag_module.ts",
1736
"feature_flag_page_component.ts",
1837
"feature_flag_page_container.ts",
38+
"types.ts",
1939
],
2040
assets = [
2141
":style",
22-
"feature_flag_page.ng.html",
42+
"feature_flag_page_component.ng.html",
2343
],
2444
deps = [
2545
"//tensorboard/webapp:app_state",
2646
"//tensorboard/webapp/angular:expect_angular_material_checkbox",
47+
"//tensorboard/webapp/feature_flag:types",
48+
"//tensorboard/webapp/feature_flag/actions",
2749
"//tensorboard/webapp/feature_flag/store",
50+
"//tensorboard/webapp/feature_flag/store:feature_flag_metadata",
2851
"//tensorboard/webapp/feature_flag/store:types",
2952
"@npm//@angular/common",
3053
"@npm//@angular/core",
3154
"@npm//@ngrx/store",
55+
"@npm//rxjs",
56+
],
57+
)
58+
59+
tf_ts_library(
60+
name = "views_test",
61+
testonly = True,
62+
srcs = [
63+
"feature_flag_modal_trigger_container_test.ts",
64+
"feature_flag_page_test.ts",
65+
],
66+
deps = [
67+
":feature_flag_modal_trigger",
68+
":views",
69+
"//tensorboard/webapp:app_state",
70+
"//tensorboard/webapp/angular:expect_angular_cdk_testing_testbed",
71+
"//tensorboard/webapp/angular:expect_angular_core_testing",
72+
"//tensorboard/webapp/angular:expect_angular_material_checkbox",
73+
"//tensorboard/webapp/angular:expect_angular_platform_browser_animations",
74+
"//tensorboard/webapp/feature_flag:types",
75+
"//tensorboard/webapp/feature_flag/actions",
76+
"//tensorboard/webapp/feature_flag/store",
77+
"//tensorboard/webapp/feature_flag/store:testing",
78+
"@npm//@angular/common",
79+
"@npm//@angular/core",
80+
"@npm//@angular/platform-browser",
81+
"@npm//@ngrx/store",
82+
"@npm//@types/jasmine",
83+
"@npm//rxjs",
3284
],
3385
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* Copyright 2022 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 {Component, OnInit} from '@angular/core';
16+
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
17+
import {Store} from '@ngrx/store';
18+
import {State} from '../../app_state';
19+
import {featureFlagOverrideChanged} from '../actions/feature_flag_actions';
20+
import {getShowFlagsEnabled} from '../store/feature_flag_selectors';
21+
import {FeatureFlagPageContainer} from './feature_flag_page_container';
22+
23+
@Component({
24+
selector: 'feature-flag-modal-trigger',
25+
template: ``,
26+
styles: [],
27+
})
28+
export class FeatureFlagModalTriggerContainer implements OnInit {
29+
readonly showFeatureFlags$ = this.store.select(getShowFlagsEnabled);
30+
private featureFlagsDialog?: MatDialogRef<FeatureFlagPageContainer>;
31+
32+
constructor(
33+
private readonly store: Store<State>,
34+
private dialog: MatDialog
35+
) {}
36+
37+
ngOnInit() {
38+
this.showFeatureFlags$.subscribe((showFeatureFlags: boolean) => {
39+
if (showFeatureFlags) {
40+
this.featureFlagsDialog = this.dialog.open(FeatureFlagPageContainer);
41+
this.featureFlagsDialog.afterClosed().subscribe(() => {
42+
// Disable the flag when the dialog is closed to prevent it from
43+
// appearing again after the page is refreshed.
44+
this.store.dispatch(
45+
featureFlagOverrideChanged({
46+
flags: {enableShowFlags: false},
47+
})
48+
);
49+
});
50+
return;
51+
}
52+
});
53+
}
54+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* Copyright 2022 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 {HarnessLoader} from '@angular/cdk/testing';
16+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
17+
import {NO_ERRORS_SCHEMA} from '@angular/core';
18+
import {ComponentFixture, TestBed} from '@angular/core/testing';
19+
import {
20+
MatDialog,
21+
MatDialogModule,
22+
MatDialogRef,
23+
MAT_DIALOG_DATA,
24+
} from '@angular/material/dialog';
25+
import {MatDialogHarness} from '@angular/material/dialog/testing';
26+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
27+
import {Store} from '@ngrx/store';
28+
import {MockStore, provideMockStore} from '@ngrx/store/testing';
29+
import {State} from '../../app_state';
30+
import {
31+
getDefaultFeatureFlags,
32+
getOverriddenFeatureFlags,
33+
getShowFlagsEnabled,
34+
} from '../store/feature_flag_selectors';
35+
import {FeatureFlags} from '../types';
36+
import {FeatureFlagModalTriggerContainer} from './feature_flag_modal_trigger_container';
37+
import {FeatureFlagPageContainer} from './feature_flag_page_container';
38+
39+
describe('feature_flag_modal_trigger_container', () => {
40+
let store: MockStore<State>;
41+
42+
let fixture: ComponentFixture<FeatureFlagModalTriggerContainer>;
43+
let loader: HarnessLoader;
44+
let rootLoader: HarnessLoader;
45+
46+
beforeEach(async () => {
47+
await TestBed.configureTestingModule({
48+
imports: [MatDialogModule, NoopAnimationsModule],
49+
declarations: [FeatureFlagPageContainer],
50+
providers: [
51+
provideMockStore(),
52+
{provide: MatDialogRef, useValue: MatDialog},
53+
],
54+
schemas: [NO_ERRORS_SCHEMA],
55+
}).compileComponents();
56+
57+
TestBed.overrideProvider(MAT_DIALOG_DATA, {useValue: {}});
58+
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
59+
60+
spyOn(store, 'dispatch').and.stub();
61+
});
62+
63+
function createComponent() {
64+
fixture = TestBed.createComponent(FeatureFlagModalTriggerContainer);
65+
loader = TestbedHarnessEnvironment.loader(fixture);
66+
rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
67+
}
68+
69+
it('creates modal when enableShowFlags is true', async () => {
70+
store.overrideSelector(getDefaultFeatureFlags, {} as FeatureFlags);
71+
store.overrideSelector(getOverriddenFeatureFlags, {});
72+
store.overrideSelector(getShowFlagsEnabled, true);
73+
createComponent();
74+
const dialog = await rootLoader.getHarness(MatDialogHarness);
75+
expect(
76+
(fixture.componentInstance as any).featureFlagsDialog
77+
).not.toBeUndefined();
78+
79+
expect(dialog).toBeDefined();
80+
});
81+
82+
it('does not create modal when enableShowFlags is false', async () => {
83+
store.overrideSelector(getDefaultFeatureFlags, {} as FeatureFlags);
84+
store.overrideSelector(getOverriddenFeatureFlags, {});
85+
store.overrideSelector(getShowFlagsEnabled, false);
86+
createComponent();
87+
expect(
88+
(fixture.componentInstance as any).featureFlagsDialog
89+
).toBeUndefined();
90+
});
91+
});

0 commit comments

Comments
 (0)