Skip to content

Commit e7b2551

Browse files
authored
core: extend page title module functionalities (#5121)
* extend page title module functionalities Extend PageTitleModule to be used by TensorBoard.corp and TensorBoard.dev surfaces with the following additional support: - Allows custom brand names as page title, e.g. `TensorBoard.corp`. - Includes non-empty experiment name in the single experiment dashboard page title.
1 parent af78290 commit e7b2551

File tree

6 files changed

+259
-9
lines changed

6 files changed

+259
-9
lines changed

tensorboard/webapp/core/BUILD

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ tf_ts_library(
2424
srcs = [
2525
"types.ts",
2626
],
27+
deps = [
28+
"@npm//@angular/core",
29+
],
2730
)
2831

2932
tf_ts_library(

tensorboard/webapp/core/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
15+
import {InjectionToken} from '@angular/core';
16+
1517
export type RunId = string;
1618

1719
export interface Run {
@@ -23,3 +25,7 @@ export enum PluginsListFailureCode {
2325
UNKNOWN = 'UNKNOWN',
2426
NOT_FOUND = 'NOT_FOUND',
2527
}
28+
29+
export const TB_BRAND_NAME = new InjectionToken<string>(
30+
'TensorBoard brand name'
31+
);

tensorboard/webapp/core/views/BUILD

+8
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ tf_ng_module(
2929
"page_title_module.ts",
3030
],
3131
deps = [
32+
"//tensorboard/webapp:selectors",
33+
"//tensorboard/webapp/app_routing:types",
3234
"//tensorboard/webapp/core:state",
35+
"//tensorboard/webapp/core:types",
3336
"//tensorboard/webapp/core/store",
3437
"@npm//@angular/common",
3538
"@npm//@angular/core",
@@ -58,20 +61,25 @@ tf_ts_library(
5861
srcs = [
5962
"dark_mode_supporter_test.ts",
6063
"hash_storage_test.ts",
64+
"page_title_test.ts",
6165
],
6266
deps = [
6367
":dark_mode_supporter",
6468
":hash_storage",
69+
":page_title",
6570
"//tensorboard/webapp:app_state",
6671
"//tensorboard/webapp:selectors",
6772
"//tensorboard/webapp/angular:expect_angular_core_testing",
6873
"//tensorboard/webapp/angular:expect_angular_platform_browser_animations",
6974
"//tensorboard/webapp/angular:expect_ngrx_store_testing",
75+
"//tensorboard/webapp/app_routing:types",
7076
"//tensorboard/webapp/core:state",
77+
"//tensorboard/webapp/core:types",
7178
"//tensorboard/webapp/core/actions",
7279
"//tensorboard/webapp/core/store",
7380
"//tensorboard/webapp/core/testing",
7481
"//tensorboard/webapp/deeplink",
82+
"//tensorboard/webapp/experiments/store:testing",
7583
"@npm//@angular/common",
7684
"@npm//@angular/compiler",
7785
"@npm//@angular/core",

tensorboard/webapp/core/views/page_title_component.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import {
2020
SimpleChanges,
2121
} from '@angular/core';
2222

23+
function setDocumentTitle(title: string) {
24+
document.title = title;
25+
}
26+
27+
const utils = {
28+
setDocumentTitle,
29+
};
30+
2331
@Component({
2432
selector: 'page-title-component',
2533
template: '',
@@ -31,7 +39,11 @@ export class PageTitleComponent implements OnChanges {
3139

3240
ngOnChanges(changes: SimpleChanges) {
3341
if (changes['title']) {
34-
document.title = changes['title'].currentValue;
42+
utils.setDocumentTitle(changes['title'].currentValue);
3543
}
3644
}
3745
}
46+
47+
export const TEST_ONLY = {
48+
utils,
49+
};

tensorboard/webapp/core/views/page_title_container.ts

+62-8
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,40 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
15-
import {ChangeDetectionStrategy, Component} from '@angular/core';
16-
import {Store, select} from '@ngrx/store';
17-
import {distinctUntilChanged, map} from 'rxjs/operators';
15+
import {
16+
ChangeDetectionStrategy,
17+
Component,
18+
Inject,
19+
Optional,
20+
} from '@angular/core';
21+
import {Store} from '@ngrx/store';
22+
import {
23+
combineLatestWith,
24+
distinctUntilChanged,
25+
map,
26+
filter,
27+
mergeMap,
28+
startWith,
29+
withLatestFrom,
30+
} from 'rxjs/operators';
31+
import {
32+
getRouteKind,
33+
getExperimentIdsFromRoute,
34+
getExperiment,
35+
} from '../../selectors';
36+
import {RouteKind} from '../../app_routing/types';
1837

1938
import {getEnvironment} from '../store';
2039
import {State} from '../state';
40+
import {TB_BRAND_NAME} from '../types';
2141

2242
/** @typehack */ import * as _typeHackRxjs from 'rxjs';
2343

44+
const DEFAULT_BRAND_NAME = 'TensorBoard';
45+
46+
/**
47+
* Renders page title.
48+
*/
2449
@Component({
2550
selector: 'page-title',
2651
template: `
@@ -36,12 +61,41 @@ import {State} from '../state';
3661
changeDetection: ChangeDetectionStrategy.OnPush,
3762
})
3863
export class PageTitleContainer {
39-
readonly title$ = this.store.pipe(
40-
select(getEnvironment),
41-
map((env) => env.window_title || 'TensorBoard'),
42-
// (it's an empty string when the `--window_title` flag is not set)
64+
private readonly getExperimentId$ = this.store.select(getRouteKind).pipe(
65+
filter((routeKind) => routeKind === RouteKind.EXPERIMENT),
66+
withLatestFrom(this.store.select(getExperimentIdsFromRoute)),
67+
map(([, experimentIds]) =>
68+
experimentIds && experimentIds.length === 1 ? experimentIds[0] : null
69+
)
70+
);
71+
72+
private readonly experimentName$ = this.getExperimentId$.pipe(
73+
filter(Boolean),
74+
mergeMap((experimentId) => {
75+
return this.store.select(getExperiment, {experimentId});
76+
}),
77+
map((experiment) => (experiment ? experiment.name : null))
78+
);
79+
80+
readonly title$ = this.store.select(getEnvironment).pipe(
81+
combineLatestWith(this.experimentName$),
82+
map(([env, experimentName]) => {
83+
const tbBrandName = this.customBrandName || DEFAULT_BRAND_NAME;
84+
if (env.window_title) {
85+
// (it's an empty string when the `--window_title` flag is not set)
86+
return env.window_title;
87+
} else if (experimentName) {
88+
return `${experimentName} - ${tbBrandName}`;
89+
} else {
90+
return tbBrandName;
91+
}
92+
}),
93+
startWith(this.customBrandName || DEFAULT_BRAND_NAME),
4394
distinctUntilChanged()
4495
);
4596

46-
constructor(private readonly store: Store<State>) {}
97+
constructor(
98+
private readonly store: Store<State>,
99+
@Optional() @Inject(TB_BRAND_NAME) private readonly customBrandName: string
100+
) {}
47101
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/* Copyright 2021 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, NO_ERRORS_SCHEMA} from '@angular/core';
16+
import {TestBed} from '@angular/core/testing';
17+
import {Store} from '@ngrx/store';
18+
import {provideMockStore, MockStore} from '@ngrx/store/testing';
19+
20+
import {State} from '../state';
21+
import {getEnvironment} from '../store';
22+
import {RouteKind} from '../../app_routing/types';
23+
import {buildExperiment} from '../../experiments/store/testing';
24+
import {
25+
getRouteKind,
26+
getExperimentIdsFromRoute,
27+
getExperiment,
28+
} from '../../selectors';
29+
import {TB_BRAND_NAME} from '../types';
30+
31+
import {PageTitleModule} from './page_title_module';
32+
import {PageTitleComponent, TEST_ONLY} from './page_title_component';
33+
import {PageTitleContainer} from './page_title_container';
34+
35+
describe('page title test', () => {
36+
let store: MockStore<State>;
37+
38+
beforeEach(async () => {
39+
await TestBed.configureTestingModule({
40+
declarations: [PageTitleComponent, PageTitleContainer],
41+
providers: [provideMockStore()],
42+
schemas: [NO_ERRORS_SCHEMA],
43+
}).compileComponents();
44+
45+
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
46+
store.overrideSelector(getRouteKind, RouteKind.EXPERIMENTS);
47+
store.overrideSelector(getExperimentIdsFromRoute, []);
48+
store.overrideSelector(getExperiment, null);
49+
store.overrideSelector(getEnvironment, {
50+
data_location: 'my-location',
51+
window_title: '',
52+
});
53+
});
54+
55+
it('uses window_title as page title if given', () => {
56+
const spy = spyOn(TEST_ONLY.utils, 'setDocumentTitle');
57+
store.overrideSelector(getRouteKind, RouteKind.EXPERIMENT);
58+
store.overrideSelector(getExperimentIdsFromRoute, ['123']);
59+
store.overrideSelector(
60+
getExperiment,
61+
buildExperiment({
62+
name: 'I will be overwritten by the window_title',
63+
})
64+
);
65+
store.overrideSelector(getEnvironment, {
66+
data_location: 'my-location',
67+
window_title: 'I am the real title',
68+
});
69+
const fixture = TestBed.createComponent(PageTitleContainer);
70+
fixture.detectChanges();
71+
72+
expect(spy).toHaveBeenCalledWith('I am the real title');
73+
});
74+
75+
it('includes experiment name in page title for experiment routes', () => {
76+
const spy = spyOn(TEST_ONLY.utils, 'setDocumentTitle');
77+
store.overrideSelector(getRouteKind, RouteKind.EXPERIMENT);
78+
store.overrideSelector(getExperimentIdsFromRoute, ['123']);
79+
store.overrideSelector(
80+
getExperiment,
81+
buildExperiment({
82+
name: 'All you need is TensorBoard',
83+
})
84+
);
85+
const fixture = TestBed.createComponent(PageTitleContainer);
86+
fixture.detectChanges();
87+
88+
expect(spy).toHaveBeenCalledWith(
89+
'All you need is TensorBoard - TensorBoard'
90+
);
91+
});
92+
93+
it('uses `Tensorboard` as default page title', () => {
94+
const spy = spyOn(TEST_ONLY.utils, 'setDocumentTitle');
95+
const fixture = TestBed.createComponent(PageTitleContainer);
96+
fixture.detectChanges();
97+
98+
expect(spy).toHaveBeenCalledWith('TensorBoard');
99+
});
100+
101+
it('uses default page title for comparison routes', () => {
102+
const spy = spyOn(TEST_ONLY.utils, 'setDocumentTitle');
103+
store.overrideSelector(getRouteKind, RouteKind.COMPARE_EXPERIMENT);
104+
const fixture = TestBed.createComponent(PageTitleContainer);
105+
fixture.detectChanges();
106+
107+
expect(spy).toHaveBeenCalledWith('TensorBoard');
108+
});
109+
});
110+
111+
@Component({
112+
selector: 'my-tester',
113+
template: ` <page-title></page-title> `,
114+
})
115+
class TestingComponent {}
116+
117+
describe('page title test with custom brand names', () => {
118+
let store: MockStore<State>;
119+
120+
beforeEach(async () => {
121+
await TestBed.configureTestingModule({
122+
imports: [PageTitleModule],
123+
declarations: [TestingComponent],
124+
providers: [
125+
provideMockStore(),
126+
{
127+
provide: TB_BRAND_NAME,
128+
useValue: 'TensorBoard.corp',
129+
},
130+
],
131+
schemas: [NO_ERRORS_SCHEMA],
132+
}).compileComponents();
133+
134+
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
135+
store.overrideSelector(getRouteKind, RouteKind.EXPERIMENTS);
136+
store.overrideSelector(getExperimentIdsFromRoute, []);
137+
store.overrideSelector(getExperiment, null);
138+
store.overrideSelector(getEnvironment, {
139+
data_location: 'my-location',
140+
window_title: '',
141+
});
142+
});
143+
144+
it('uses TensorBoard brand name as page title as default', () => {
145+
const spy = spyOn(TEST_ONLY.utils, 'setDocumentTitle');
146+
const fixture = TestBed.createComponent(TestingComponent);
147+
fixture.detectChanges();
148+
149+
expect(spy).toHaveBeenCalledWith('TensorBoard.corp');
150+
});
151+
152+
it('specifies TensorBoard brand name in page title after experiment name', () => {
153+
const spy = spyOn(TEST_ONLY.utils, 'setDocumentTitle');
154+
store.overrideSelector(getRouteKind, RouteKind.EXPERIMENT);
155+
store.overrideSelector(getExperimentIdsFromRoute, ['123']);
156+
store.overrideSelector(
157+
getExperiment,
158+
buildExperiment({
159+
name: 'Testing Brand Name',
160+
})
161+
);
162+
const fixture = TestBed.createComponent(TestingComponent);
163+
fixture.detectChanges();
164+
165+
expect(spy).toHaveBeenCalledWith('Testing Brand Name - TensorBoard.corp');
166+
});
167+
});

0 commit comments

Comments
 (0)