Skip to content

Commit ea145a4

Browse files
authored
routing: improve compare navigation (#4908)
When wanting to navigate to a compare route with a programmatical API, we need to form the navigation parameter correctly, knowing its encoding scheme. Not wanting to leak the business logic for encoding alias and id, we now changed the interface for the compare route navigation with a translation to an AppRouting Route in the AppRouting effects.
1 parent 6bac32d commit ea145a4

File tree

6 files changed

+127
-13
lines changed

6 files changed

+127
-13
lines changed

tensorboard/webapp/app_routing/effects/app_routing_effects.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ 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 {Inject, Injectable} from '@angular/core';
15+
import {Injectable} from '@angular/core';
1616
import {Actions, createEffect, ofType} from '@ngrx/effects';
1717
import {Action, createAction, Store} from '@ngrx/store';
1818
import {merge, Observable, of} from 'rxjs';
@@ -34,13 +34,17 @@ import {
3434
stateRehydratedFromUrl,
3535
} from '../actions';
3636
import {AppRootProvider} from '../app_root';
37-
import {areRoutesEqual, getRouteId} from '../internal_utils';
37+
import {
38+
areRoutesEqual,
39+
getRouteId,
40+
serializeCompareExperimentParams,
41+
} from '../internal_utils';
3842
import {Location} from '../location';
3943
import {ProgrammaticalNavigationModule} from '../programmatical_navigation_module';
4044
import {RouteConfigs} from '../route_config';
4145
import {RouteRegistryModule} from '../route_registry_module';
4246
import {getActiveRoute} from '../store/app_routing_selectors';
43-
import {Navigation, Route} from '../types';
47+
import {Navigation, Route, RouteKind, RouteParams} from '../types';
4448

4549
/** @typehack */ import * as _typeHackNgrxEffects from '@ngrx/effects';
4650
/** @typehack */ import * as _typeHackModels from '@ngrx/store/src/models';
@@ -144,7 +148,26 @@ export class AppRoutingEffects {
144148
return nav !== null;
145149
}),
146150
map((programmaticalNavigation) => {
147-
const {routeKind, routeParams} = programmaticalNavigation!;
151+
const nav = programmaticalNavigation!;
152+
const routeKind = nav.routeKind;
153+
154+
// TODO(stephanwlee): currently, the RouteParams is ill-typed and you can
155+
// currently add any property without any type error. Better type it.
156+
let routeParams: RouteParams;
157+
switch (nav.routeKind) {
158+
case RouteKind.COMPARE_EXPERIMENT:
159+
routeParams = {
160+
experimentIds: serializeCompareExperimentParams(
161+
nav.routeParams.aliasAndExperimentIds
162+
),
163+
};
164+
break;
165+
default:
166+
routeParams = nav.routeParams;
167+
}
168+
return {routeKind, routeParams};
169+
}),
170+
map(({routeKind, routeParams}) => {
148171
const routeMatch = this.routeConfigs
149172
? this.routeConfigs.matchByRouteKind(routeKind, routeParams)
150173
: null;

tensorboard/webapp/app_routing/effects/app_routing_effects_test.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616
import {Component} from '@angular/core';
1717
import {fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
1818
import {provideMockActions} from '@ngrx/effects/testing';
19-
import {Action, createAction, Store} from '@ngrx/store';
19+
import {Action, createAction, props, Store} from '@ngrx/store';
2020
import {MockStore, provideMockStore} from '@ngrx/store/testing';
2121
import {of, ReplaySubject} from 'rxjs';
2222

@@ -26,6 +26,7 @@ import {navigationRequested} from '../actions';
2626
import {AppRootProvider, TestableAppRootProvider} from '../app_root';
2727
import {Location} from '../location';
2828
import {
29+
NavigateToCompare,
2930
NavigateToExperiments,
3031
ProgrammaticalNavigationModule,
3132
} from '../programmatical_navigation_module';
@@ -40,6 +41,10 @@ import {AppRoutingEffects, TEST_ONLY} from './app_routing_effects';
4041
class TestableComponent {}
4142

4243
const testAction = createAction('[TEST] test actions');
44+
const testNavToCompareAction = createAction(
45+
'[TEST] test nav to compare',
46+
props<NavigateToCompare['routeParams']>()
47+
);
4348

4449
describe('app_routing_effects', () => {
4550
let effects: AppRoutingEffects;
@@ -87,10 +92,10 @@ describe('app_routing_effects', () => {
8792
];
8893
}
8994

90-
function programmaticalNavigationFactory() {
95+
function programmaticalNavToListviewFactory() {
9196
return {
9297
actionCreator: testAction,
93-
lambda: (action: typeof testAction) => {
98+
lambda: (action: ReturnType<typeof testAction>) => {
9499
return {
95100
routeKind: RouteKind.EXPERIMENTS,
96101
routeParams: {},
@@ -99,11 +104,26 @@ describe('app_routing_effects', () => {
99104
};
100105
}
101106

107+
function programmaticalCompareNavigationFactory() {
108+
return {
109+
actionCreator: testNavToCompareAction,
110+
lambda: (action: ReturnType<typeof testNavToCompareAction>) => {
111+
return {
112+
routeKind: RouteKind.COMPARE_EXPERIMENT,
113+
routeParams: action,
114+
} as NavigateToCompare;
115+
},
116+
};
117+
}
118+
102119
await TestBed.configureTestingModule({
103120
imports: [
104121
RouteRegistryModule.registerRoutes(routeFactory),
105122
ProgrammaticalNavigationModule.registerProgrammaticalNavigation(
106-
programmaticalNavigationFactory
123+
programmaticalNavToListviewFactory
124+
),
125+
ProgrammaticalNavigationModule.registerProgrammaticalNavigation(
126+
programmaticalCompareNavigationFactory
107127
),
108128
],
109129
providers: [
@@ -539,6 +559,33 @@ describe('app_routing_effects', () => {
539559
}),
540560
]);
541561
});
562+
563+
it('translates programmatical compare nav correctly', () => {
564+
store.overrideSelector(getActiveRoute, null);
565+
store.refreshState();
566+
567+
action.next(
568+
testNavToCompareAction({
569+
aliasAndExperimentIds: [
570+
{alias: 'bar', id: 'foo'},
571+
{alias: 'omega', id: 'alpha'},
572+
],
573+
})
574+
);
575+
576+
expect(actualActions).toEqual([
577+
actions.navigating({
578+
after: buildRoute({
579+
routeKind: RouteKind.COMPARE_EXPERIMENT,
580+
params: {
581+
experimentIds: 'bar:foo,omega:alpha',
582+
},
583+
pathname: '/compare',
584+
queryParams: [],
585+
}),
586+
}),
587+
]);
588+
});
542589
});
543590
});
544591

tensorboard/webapp/app_routing/internal_utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export function parseCompareExperimentStr(
5454
});
5555
}
5656

57+
export function serializeCompareExperimentParams(
58+
params: Array<{alias: string; id: string}>
59+
): string {
60+
return params.map(({alias, id}) => `${alias}:${id}`).join(',');
61+
}
62+
5763
/**
5864
* Returns experimentIds from route parameter. For a route that does not contain
5965
* any experiment ids, it returns null.

tensorboard/webapp/app_routing/internal_utils_test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,35 @@ describe('app_routing/utils', () => {
4444
});
4545
});
4646

47+
describe('#serializeCompareExperimentParams', () => {
48+
it('serializes to empty string with an empty input', () => {
49+
expect(utils.serializeCompareExperimentParams([])).toBe('');
50+
});
51+
52+
it('serializes alias and displayName', () => {
53+
const input = [
54+
{alias: 'foo', id: 'bar'},
55+
{alias: 'baz', id: 'baz'},
56+
];
57+
expect(utils.serializeCompareExperimentParams(input)).toBe(
58+
'foo:bar,baz:baz'
59+
);
60+
});
61+
62+
it('does not deduplicate or validate', () => {
63+
const input = [
64+
{alias: 'foo', id: 'bar'},
65+
// does not deduplicate
66+
{alias: 'tar', id: 'bar'},
67+
{alias: 'foo', id: 'bang'},
68+
{alias: '', id: ''},
69+
];
70+
expect(utils.serializeCompareExperimentParams(input)).toBe(
71+
'foo:bar,tar:bar,foo:bang,:'
72+
);
73+
});
74+
});
75+
4776
describe('#getExperimentIdsFromRouteParams', () => {
4877
it('returns ids from compare route', () => {
4978
const actual = utils.getExperimentIdsFromRouteParams(

tensorboard/webapp/app_routing/programmatical_navigation_module_test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,18 @@ describe('programmatical navigation module test', () => {
101101
function provider2() {
102102
return {
103103
actionCreator: testAction,
104-
lambda: (action: typeof testAction) => {
104+
lambda: (action: typeof testAction): NavigateToCompare => {
105105
return {
106106
routeKind: RouteKind.COMPARE_EXPERIMENT,
107-
routeParams: {experimentIds: 'foo'},
108-
} as NavigateToCompare;
107+
routeParams: {
108+
aliasAndExperimentIds: [
109+
{
110+
alias: 'Foo',
111+
id: 'foo',
112+
},
113+
],
114+
},
115+
};
109116
},
110117
};
111118
}

tensorboard/webapp/app_routing/programmatical_navigation_types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
import {InjectionToken} from '@angular/core';
1616
import {Action, ActionCreator, Creator} from '@ngrx/store';
1717

18-
import {CompareRouteParams, ExperimentRouteParams, RouteKind} from './types';
18+
import {ExperimentRouteParams, RouteKind} from './types';
1919

2020
export const NAVIGATION_PROVIDER = new InjectionToken<NavigationLambda[]>(
2121
'[App Routing] Programmatical Navigation Provider'
@@ -28,7 +28,9 @@ export interface NavigateToExperiment {
2828

2929
export interface NavigateToCompare {
3030
routeKind: RouteKind.COMPARE_EXPERIMENT;
31-
routeParams: CompareRouteParams;
31+
routeParams: {
32+
aliasAndExperimentIds: Array<{alias: string; id: string}>;
33+
};
3234
}
3335

3436
export interface NavigateToExperiments {

0 commit comments

Comments
 (0)