diff --git a/tensorboard/BUILD b/tensorboard/BUILD index 48291ad2e2..b81bb03b8e 100644 --- a/tensorboard/BUILD +++ b/tensorboard/BUILD @@ -212,6 +212,7 @@ py_library( visibility = ["//visibility:public"], deps = [ "//tensorboard:expect_pkg_resources_installed", + "//tensorboard/backend:experimental_plugin", "//tensorboard/plugins:base_plugin", "//tensorboard/plugins/audio:audio_plugin", "//tensorboard/plugins/beholder:beholder_plugin_loader", diff --git a/tensorboard/default.py b/tensorboard/default.py index 1938acf85f..6202d01d06 100644 --- a/tensorboard/default.py +++ b/tensorboard/default.py @@ -33,6 +33,7 @@ import pkg_resources +from tensorboard.backend import experimental_plugin from tensorboard.compat import tf from tensorboard.plugins import base_plugin from tensorboard.plugins.audio import audio_plugin @@ -55,13 +56,22 @@ logger = logging.getLogger(__name__) + +class ExperimentalDebuggerV2Plugin( + debugger_v2_plugin.DebuggerV2Plugin, experimental_plugin.ExperimentalPlugin +): + """Debugger v2 plugin marked as experimental.""" + + pass + + # Ordering matters. The order in which these lines appear determines the # ordering of tabs in TensorBoard's GUI. _PLUGINS = [ core_plugin.CorePluginLoader, scalars_plugin.ScalarsPlugin, custom_scalars_plugin.CustomScalarsPlugin, - debugger_v2_plugin.DebuggerV2Plugin, + ExperimentalDebuggerV2Plugin, images_plugin.ImagesPlugin, audio_plugin.AudioPlugin, debugger_plugin_loader.DebuggerPluginLoader, diff --git a/tensorboard/webapp/BUILD b/tensorboard/webapp/BUILD index 4d359b2772..408a527078 100644 --- a/tensorboard/webapp/BUILD +++ b/tensorboard/webapp/BUILD @@ -49,6 +49,17 @@ tf_ts_library( ], ) +ng_module( + name = "app_state", + srcs = [ + "app_state.ts", + ], + deps = [ + "//tensorboard/webapp/core/store", + "//tensorboard/webapp/feature_flag/store:types", + ], +) + # Wrapper that prepares Angular app for deployment, dealing with browser # compatibility, Vulcanization, and configuration (e.g. prod vs. dev). ng_module( @@ -147,6 +158,7 @@ tf_ng_web_test_suite( "//tensorboard/webapp/reloader:test_lib", "//tensorboard/webapp/settings:test_lib", "//tensorboard/webapp/webapp_data_source:feature_flag_test_lib", + "//tensorboard/webapp/webapp_data_source:webapp_data_source_test_lib", ], ) diff --git a/tensorboard/webapp/app_state.ts b/tensorboard/webapp/app_state.ts new file mode 100644 index 0000000000..a9f77d0ae7 --- /dev/null +++ b/tensorboard/webapp/app_state.ts @@ -0,0 +1,19 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +import {State as CoreState} from './core/store/core_types'; +import {State as FeatureFlagState} from './feature_flag/store/feature_flag_types'; + +export type State = CoreState & FeatureFlagState; diff --git a/tensorboard/webapp/core/effects/BUILD b/tensorboard/webapp/core/effects/BUILD index 9acd97f6cb..f57e5ec983 100644 --- a/tensorboard/webapp/core/effects/BUILD +++ b/tensorboard/webapp/core/effects/BUILD @@ -10,8 +10,10 @@ ng_module( "index.ts", ], deps = [ + "//tensorboard/webapp:app_state", "//tensorboard/webapp/core/actions", "//tensorboard/webapp/core/store", + "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/types", "//tensorboard/webapp/webapp_data_source", "@npm//@angular/common", @@ -30,11 +32,13 @@ tf_ts_library( ], deps = [ ":effects", + "//tensorboard/webapp:app_state", "//tensorboard/webapp/angular:expect_angular_core_testing", "//tensorboard/webapp/angular:expect_ngrx_store_testing", "//tensorboard/webapp/core/actions", "//tensorboard/webapp/core/store", "//tensorboard/webapp/core/testing", + "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/types", "//tensorboard/webapp/webapp_data_source", "//tensorboard/webapp/webapp_data_source:http_client_testing", diff --git a/tensorboard/webapp/core/effects/core_effects.ts b/tensorboard/webapp/core/effects/core_effects.ts index 0c3db63b39..946f6f6fe6 100644 --- a/tensorboard/webapp/core/effects/core_effects.ts +++ b/tensorboard/webapp/core/effects/core_effects.ts @@ -34,8 +34,9 @@ import { } from '../actions'; import {getPluginsListLoaded} from '../store'; import {DataLoadState} from '../../types/data'; -import {State} from '../store/core_types'; import {TBServerDataSource} from '../../webapp_data_source/tb_server_data_source'; +import {getEnabledExperimentalPlugins} from '../../feature_flag/store/feature_flag_selectors'; +import {State} from '../../app_state'; /** @typehack */ import * as _typeHackRxjs from 'rxjs'; /** @typehack */ import * as _typeHackNgrx from '@ngrx/store/src/models'; @@ -51,12 +52,15 @@ export class CoreEffects { readonly loadPluginsListing$ = createEffect(() => this.actions$.pipe( ofType(coreLoaded, reload, manualReload), - withLatestFrom(this.store.select(getPluginsListLoaded)), + withLatestFrom( + this.store.select(getPluginsListLoaded), + this.store.select(getEnabledExperimentalPlugins) + ), filter(([, {state}]) => state !== DataLoadState.LOADING), tap(() => this.store.dispatch(pluginsListingRequested())), - mergeMap(() => { + mergeMap(([, , enabledExperimentalPlugins]) => { return zip( - this.webappDataSource.fetchPluginsListing(), + this.webappDataSource.fetchPluginsListing(enabledExperimentalPlugins), this.webappDataSource.fetchRuns(), this.webappDataSource.fetchEnvironments() ).pipe( diff --git a/tensorboard/webapp/core/effects/core_effects_test.ts b/tensorboard/webapp/core/effects/core_effects_test.ts index e9d9e48efd..d4f3ad8907 100644 --- a/tensorboard/webapp/core/effects/core_effects_test.ts +++ b/tensorboard/webapp/core/effects/core_effects_test.ts @@ -21,13 +21,14 @@ import {ReplaySubject, of} from 'rxjs'; import {CoreEffects} from './core_effects'; import * as coreActions from '../actions'; -import {State} from '../store'; +import {State} from '../../app_state'; import {createPluginMetadata, createState, createCoreState} from '../testing'; import {PluginsListing} from '../../types/api'; import {DataLoadState} from '../../types/data'; import {TBServerDataSource} from '../../webapp_data_source/tb_server_data_source'; +import {getEnabledExperimentalPlugins} from '../../feature_flag/store/feature_flag_selectors'; import { TBHttpClientTestingModule, HttpTestingController, @@ -37,7 +38,7 @@ describe('core_effects', () => { let httpMock: HttpTestingController; let coreEffects: CoreEffects; let action: ReplaySubject; - let store: MockStore; + let store: MockStore>; let fetchRuns: jasmine.Spy; let fetchEnvironments: jasmine.Spy; let dispatchSpy: jasmine.Spy; @@ -74,6 +75,8 @@ describe('core_effects', () => { fetchEnvironments = spyOn(dataSource, 'fetchEnvironments') .withArgs() .and.returnValue(of(null)); + + store.overrideSelector(getEnabledExperimentalPlugins, []); }); afterEach(() => { @@ -96,6 +99,9 @@ describe('core_effects', () => { }); it('fetches plugins listing and fires success action', () => { + store.overrideSelector(getEnabledExperimentalPlugins, []); + store.refreshState(); + const pluginsListing: PluginsListing = { core: createPluginMetadata('Core'), }; @@ -118,6 +124,44 @@ describe('core_effects', () => { expect(recordedActions).toEqual([expected]); }); + it( + 'appends query params to the data/plugins_listing when ' + + 'getEnabledExperimentalPlugins is non-empty', + () => { + store.overrideSelector(getEnabledExperimentalPlugins, [ + 'alpha', + 'beta', + ]); + store.refreshState(); + + const pluginsListing: PluginsListing = { + core: createPluginMetadata('Core'), + }; + + action.next(onAction); + // Flushing the request response invokes above subscription sychronously. + httpMock + .expectOne( + 'data/plugins_listing?experimentalPlugin=alpha&' + + 'experimentalPlugin=beta' + ) + .flush(pluginsListing); + + expect(fetchRuns).toHaveBeenCalled(); + expect(fetchEnvironments).toHaveBeenCalled(); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + coreActions.pluginsListingRequested() + ); + + const expected = coreActions.pluginsListingLoaded({ + plugins: pluginsListing, + }); + expect(recordedActions).toEqual([expected]); + } + ); + it('ignores the action when loadState is loading', () => { store.setState( createState( diff --git a/tensorboard/webapp/feature_flag/store/BUILD b/tensorboard/webapp/feature_flag/store/BUILD index 45ada35bcd..be61c9a6e8 100644 --- a/tensorboard/webapp/feature_flag/store/BUILD +++ b/tensorboard/webapp/feature_flag/store/BUILD @@ -7,9 +7,10 @@ ng_module( srcs = [ "feature_flag_reducers.ts", "feature_flag_selectors.ts", - "feature_flag_types.ts", ], deps = [ + ":types", + "//tensorboard/webapp:app_state", "//tensorboard/webapp/feature_flag:types", "//tensorboard/webapp/feature_flag/actions", "//tensorboard/webapp/webapp_data_source:feature_flag", @@ -18,6 +19,16 @@ ng_module( ], ) +ng_module( + name = "types", + srcs = [ + "feature_flag_types.ts", + ], + deps = [ + "//tensorboard/webapp/feature_flag:types", + ], +) + ng_module( name = "store_test_lib", testonly = True, @@ -28,6 +39,7 @@ ng_module( ], deps = [ ":store", + ":types", "//tensorboard/webapp/feature_flag:types", "//tensorboard/webapp/feature_flag/actions", "@npm//@ngrx/store", diff --git a/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts b/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts index 954b688373..ad3e336033 100644 --- a/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts +++ b/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts @@ -38,6 +38,9 @@ export const getFeature = createSelector( } ); -export function getEnabledExperimentalPlugins(state: State): string[] { - return getFeature(state, 'enabledExperimentalPlugins') as string[]; -} +export const getEnabledExperimentalPlugins = createSelector( + selectFeatureFlagState, + (state) => { + return state.enabledExperimentalPlugins as string[]; + } +); diff --git a/tensorboard/webapp/webapp_data_source/BUILD b/tensorboard/webapp/webapp_data_source/BUILD index 0e16fe76df..97ca07e8a1 100644 --- a/tensorboard/webapp/webapp_data_source/BUILD +++ b/tensorboard/webapp/webapp_data_source/BUILD @@ -17,6 +17,20 @@ ng_module( ], ) +ng_module( + name = "webapp_data_source_test_lib", + testonly = True, + srcs = [ + "tb_server_data_source_test.ts", + ], + deps = [ + ":http_client_testing", + ":webapp_data_source", + "@npm//@angular/core", + "@npm//@types/jasmine", + ], +) + ng_module( name = "http_client", srcs = [ diff --git a/tensorboard/webapp/webapp_data_source/tb_server_data_source.ts b/tensorboard/webapp/webapp_data_source/tb_server_data_source.ts index 32b004d812..14c8cb22e5 100644 --- a/tensorboard/webapp/webapp_data_source/tb_server_data_source.ts +++ b/tensorboard/webapp/webapp_data_source/tb_server_data_source.ts @@ -21,6 +21,18 @@ import {TBHttpClient} from './tb_http_client'; /** @typehack */ import * as _typeHackRxjs from 'rxjs'; +function getPluginsListingQueryParams(enabledExperimentPluginIds: string[]) { + if (!enabledExperimentPluginIds.length) { + return null; + } + + const params = new URLSearchParams(); + for (const pluginId of enabledExperimentPluginIds) { + params.append('experimentalPlugin', pluginId); + } + return params; +} + @Injectable() export class TBServerDataSource { // TODO(soergel): implements WebappDataSource @@ -28,8 +40,12 @@ export class TBServerDataSource { constructor(private http: TBHttpClient) {} - fetchPluginsListing() { - return this.http.get('data/plugins_listing'); + fetchPluginsListing(enabledExperimentPluginIds: string[]) { + const params = getPluginsListingQueryParams(enabledExperimentPluginIds); + const pathWithParams = params + ? `data/plugins_listing?${params.toString()}` + : 'data/plugins_listing'; + return this.http.get(pathWithParams); } fetchRuns() { diff --git a/tensorboard/webapp/webapp_data_source/tb_server_data_source_test.ts b/tensorboard/webapp/webapp_data_source/tb_server_data_source_test.ts new file mode 100644 index 0000000000..31781c4c09 --- /dev/null +++ b/tensorboard/webapp/webapp_data_source/tb_server_data_source_test.ts @@ -0,0 +1,54 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +import {TestBed} from '@angular/core/testing'; + +import {TBServerDataSource} from './tb_server_data_source'; +import { + TBHttpClientTestingModule, + HttpTestingController, +} from './tb_http_client_testing'; + +describe('tb_server_data_source', () => { + describe('TBServerDataSource', () => { + let dataSource: TBServerDataSource; + let httpMock: HttpTestingController; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TBHttpClientTestingModule], + providers: [TBServerDataSource], + }).compileComponents(); + + httpMock = TestBed.get(HttpTestingController); + dataSource = TestBed.get(TBServerDataSource); + }); + + describe('fetchPluginsListing', () => { + it('fetches from "data/plugins_listing"', () => { + dataSource.fetchPluginsListing([]).subscribe(jasmine.createSpy()); + httpMock.expectOne('data/plugins_listing'); + }); + + it('passes query parameter, "experimentalPlugin"', () => { + dataSource + .fetchPluginsListing(['foo', 'bar']) + .subscribe(jasmine.createSpy()); + httpMock.expectOne( + 'data/plugins_listing?experimentalPlugin=foo&experimentalPlugin=bar' + ); + }); + }); + }); +});