diff --git a/tensorboard/webapp/feature_flag/BUILD b/tensorboard/webapp/feature_flag/BUILD index 77ce87c6395..0c22963d737 100644 --- a/tensorboard/webapp/feature_flag/BUILD +++ b/tensorboard/webapp/feature_flag/BUILD @@ -8,6 +8,7 @@ tf_ng_module( "feature_flag_module.ts", ], deps = [ + ":force_svg_data_source", "//tensorboard/webapp/feature_flag/effects", "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/feature_flag/store:types", @@ -19,6 +20,18 @@ tf_ng_module( ], ) +tf_ng_module( + name = "force_svg_data_source", + srcs = [ + "force_svg_data_source.ts", + "force_svg_data_source_module.ts", + ], + deps = [ + "//tensorboard/webapp/webapp_data_source:feature_flag_types", + "@npm//@angular/core", + ], +) + tf_ts_library( name = "types", srcs = [ @@ -29,6 +42,16 @@ tf_ts_library( tf_ts_library( name = "testing", testonly = True, - srcs = ["testing.ts"], - deps = [":types"], + srcs = [ + "force_svg_data_source_test.ts", + "testing.ts", + ], + deps = [ + ":force_svg_data_source", + ":types", + "//tensorboard/webapp/angular:expect_angular_core_testing", + "//tensorboard/webapp/util:local_storage_testing", + "//tensorboard/webapp/webapp_data_source:feature_flag_types", + "@npm//@types/jasmine", + ], ) diff --git a/tensorboard/webapp/feature_flag/effects/BUILD b/tensorboard/webapp/feature_flag/effects/BUILD index 8fe18308001..91ff8480a37 100644 --- a/tensorboard/webapp/feature_flag/effects/BUILD +++ b/tensorboard/webapp/feature_flag/effects/BUILD @@ -8,9 +8,11 @@ tf_ng_module( "feature_flag_effects.ts", ], deps = [ + "//tensorboard/webapp/feature_flag:force_svg_data_source", "//tensorboard/webapp/feature_flag/actions", "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/feature_flag/store:types", + "//tensorboard/webapp/webapp_data_source:feature_flag", "//tensorboard/webapp/webapp_data_source:feature_flag_types", "@npm//@angular/core", "@npm//@ngrx/effects", @@ -29,7 +31,9 @@ tf_ng_module( ":effects", "//tensorboard/webapp/angular:expect_angular_core_testing", "//tensorboard/webapp/angular:expect_ngrx_store_testing", + "//tensorboard/webapp/feature_flag:force_svg_data_source", "//tensorboard/webapp/feature_flag:testing", + "//tensorboard/webapp/feature_flag:types", "//tensorboard/webapp/feature_flag/actions", "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/feature_flag/store:types", diff --git a/tensorboard/webapp/feature_flag/effects/feature_flag_effects.ts b/tensorboard/webapp/feature_flag/effects/feature_flag_effects.ts index 226c6291583..b47665cf9a7 100644 --- a/tensorboard/webapp/feature_flag/effects/feature_flag_effects.ts +++ b/tensorboard/webapp/feature_flag/effects/feature_flag_effects.ts @@ -19,6 +19,7 @@ import {Action, createAction, Store} from '@ngrx/store'; import {combineLatestWith, map} from 'rxjs/operators'; import {TBFeatureFlagDataSource} from '../../webapp_data_source/tb_feature_flag_data_source_types'; import {partialFeatureFlagsLoaded} from '../actions/feature_flag_actions'; +import {ForceSvgDataSource} from '../force_svg_data_source'; import {getIsAutoDarkModeAllowed} from '../store/feature_flag_selectors'; import {State} from '../store/feature_flag_types'; @@ -33,6 +34,13 @@ export class FeatureFlagEffects { combineLatestWith(this.store.select(getIsAutoDarkModeAllowed)), map(([, isDarkModeAllowed]) => { const features = this.dataSource.getFeatures(isDarkModeAllowed); + + if (features.forceSvg != null) { + this.forceSvgDataSource.updateForceSvgFlag(features.forceSvg); + } else { + features.forceSvg = this.forceSvgDataSource.getForceSvgFlag(); + } + return partialFeatureFlagsLoaded({features}); }) ) @@ -41,7 +49,8 @@ export class FeatureFlagEffects { constructor( private readonly actions$: Actions, private readonly store: Store, - private readonly dataSource: TBFeatureFlagDataSource + private readonly dataSource: TBFeatureFlagDataSource, + private readonly forceSvgDataSource: ForceSvgDataSource ) {} /** @export */ diff --git a/tensorboard/webapp/feature_flag/effects/feature_flag_effects_test.ts b/tensorboard/webapp/feature_flag/effects/feature_flag_effects_test.ts index 54fa78b5660..a2e4b1b16f7 100644 --- a/tensorboard/webapp/feature_flag/effects/feature_flag_effects_test.ts +++ b/tensorboard/webapp/feature_flag/effects/feature_flag_effects_test.ts @@ -23,21 +23,25 @@ import { TestingTBFeatureFlagDataSource, } from '../../webapp_data_source/tb_feature_flag_testing'; import {partialFeatureFlagsLoaded} from '../actions/feature_flag_actions'; +import {ForceSvgDataSource} from '../force_svg_data_source'; +import {ForceSvgDataSourceModule} from '../force_svg_data_source_module'; import {getIsAutoDarkModeAllowed} from '../store/feature_flag_selectors'; import {State} from '../store/feature_flag_types'; import {buildFeatureFlag} from '../testing'; +import {FeatureFlags} from '../types'; import {FeatureFlagEffects} from './feature_flag_effects'; describe('feature_flag_effects', () => { let actions: ReplaySubject; let store: MockStore; let dataSource: TestingTBFeatureFlagDataSource; + let forceSvgDataSource: ForceSvgDataSource; let effects: FeatureFlagEffects; beforeEach(async () => { actions = new ReplaySubject(1); await TestBed.configureTestingModule({ - imports: [TBFeatureFlagTestingModule], + imports: [TBFeatureFlagTestingModule, ForceSvgDataSourceModule], providers: [ provideMockActions(actions), FeatureFlagEffects, @@ -47,6 +51,7 @@ describe('feature_flag_effects', () => { effects = TestBed.inject(FeatureFlagEffects); store = TestBed.inject>(Store) as MockStore; dataSource = TestBed.inject(TestingTBFeatureFlagDataSource); + forceSvgDataSource = TestBed.inject(ForceSvgDataSource); store.overrideSelector(getIsAutoDarkModeAllowed, false); }); @@ -79,5 +84,55 @@ describe('feature_flag_effects', () => { }), ]); }); + + it('calls updateForceSvgFlag when getFeatures returns a value for forceSvg', () => { + spyOn(dataSource, 'getFeatures').and.returnValue( + buildFeatureFlag({ + forceSvg: true, + }) + ); + let updateSpy = spyOn( + forceSvgDataSource, + 'updateForceSvgFlag' + ).and.stub(); + + actions.next(effects.ngrxOnInitEffects()); + + expect(recordedActions).toEqual([ + partialFeatureFlagsLoaded({ + features: buildFeatureFlag({ + forceSvg: true, + }), + }), + ]); + + expect(updateSpy).toHaveBeenCalledOnceWith(true); + }); + + it('gets forceSVG flag from ForceSvgDataSource when getFeatures returns no value for forceSvg', () => { + let featureFlags: Partial = buildFeatureFlag(); + delete featureFlags.forceSvg; + spyOn(dataSource, 'getFeatures').and.returnValue(featureFlags); + let updateSpy = spyOn( + forceSvgDataSource, + 'updateForceSvgFlag' + ).and.stub(); + let getSpy = spyOn(forceSvgDataSource, 'getForceSvgFlag').and.returnValue( + true + ); + + actions.next(effects.ngrxOnInitEffects()); + + expect(recordedActions).toEqual([ + partialFeatureFlagsLoaded({ + features: buildFeatureFlag({ + forceSvg: true, + }), + }), + ]); + + expect(getSpy).toHaveBeenCalledOnceWith(); + expect(updateSpy).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/tensorboard/webapp/feature_flag/feature_flag_module.ts b/tensorboard/webapp/feature_flag/feature_flag_module.ts index 1bee177bb1d..7a3f26f741e 100644 --- a/tensorboard/webapp/feature_flag/feature_flag_module.ts +++ b/tensorboard/webapp/feature_flag/feature_flag_module.ts @@ -23,6 +23,7 @@ import { } from '../persistent_settings'; import {TBFeatureFlagModule} from '../webapp_data_source/tb_feature_flag_module'; import {FeatureFlagEffects} from './effects/feature_flag_effects'; +import {ForceSvgDataSourceModule} from './force_svg_data_source_module'; import {reducers} from './store/feature_flag_reducers'; import {getEnableDarkModeOverride} from './store/feature_flag_selectors'; import { @@ -44,6 +45,7 @@ export function getThemeSettingSelector() { @NgModule({ imports: [ + ForceSvgDataSourceModule, TBFeatureFlagModule, StoreModule.forFeature( FEATURE_FLAG_FEATURE_KEY, diff --git a/tensorboard/webapp/feature_flag/force_svg_data_source.ts b/tensorboard/webapp/feature_flag/force_svg_data_source.ts new file mode 100644 index 00000000000..ba15022bc53 --- /dev/null +++ b/tensorboard/webapp/feature_flag/force_svg_data_source.ts @@ -0,0 +1,41 @@ +/* Copyright 2022 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 {Injectable} from '@angular/core'; + +const FORCE_SVG_RENDERER_KEY = '_tb_force_svg'; + +@Injectable() +export class ForceSvgDataSource { + constructor() {} + + getForceSvgFlag(): boolean { + if (localStorage.getItem(FORCE_SVG_RENDERER_KEY)) { + return true; + } + return false; + } + + updateForceSvgFlag(forceSvgFlag: boolean) { + if (forceSvgFlag) { + localStorage.setItem(FORCE_SVG_RENDERER_KEY, 'present'); + } else { + localStorage.removeItem(FORCE_SVG_RENDERER_KEY); + } + } +} + +export const TEST_ONLY = { + FORCE_SVG_RENDERER_KEY, +}; diff --git a/tensorboard/webapp/feature_flag/force_svg_data_source_module.ts b/tensorboard/webapp/feature_flag/force_svg_data_source_module.ts new file mode 100644 index 00000000000..dcc995d8352 --- /dev/null +++ b/tensorboard/webapp/feature_flag/force_svg_data_source_module.ts @@ -0,0 +1,22 @@ +/* Copyright 2022 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 {NgModule} from '@angular/core'; +import {ForceSvgDataSource} from './force_svg_data_source'; + +@NgModule({ + providers: [ForceSvgDataSource], +}) +export class ForceSvgDataSourceModule {} diff --git a/tensorboard/webapp/feature_flag/force_svg_data_source_test.ts b/tensorboard/webapp/feature_flag/force_svg_data_source_test.ts new file mode 100644 index 00000000000..d506069ad89 --- /dev/null +++ b/tensorboard/webapp/feature_flag/force_svg_data_source_test.ts @@ -0,0 +1,77 @@ +/* Copyright 2022 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 {LocalStorageTestingModule} from '../util/local_storage_testing'; +import {ForceSvgDataSource, TEST_ONLY} from './force_svg_data_source'; + +describe('feature_flag/force_svg_util test', () => { + let dataSource: ForceSvgDataSource; + let getItemReturnValue: string | null; + let getItemSpy: jasmine.Spy; + let setItemSpy: jasmine.Spy; + let removeItemSpy: jasmine.Spy; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LocalStorageTestingModule], + providers: [ForceSvgDataSource], + }).compileComponents(); + + getItemSpy = spyOn(window.localStorage, 'getItem').and.callFake( + (key: string) => { + return getItemReturnValue; + } + ); + setItemSpy = spyOn(window.localStorage, 'setItem').and.stub(); + removeItemSpy = spyOn(window.localStorage, 'removeItem').and.stub(); + dataSource = TestBed.inject(ForceSvgDataSource); + }); + + describe('#getForceSVG', () => { + it('returns false if localStorage.getItem returns null', () => { + getItemReturnValue = null; + const actual = dataSource.getForceSvgFlag(); + expect(getItemSpy).toHaveBeenCalledOnceWith( + TEST_ONLY.FORCE_SVG_RENDERER_KEY + ); + expect(actual).toBeFalse(); + }); + + it('returns true if there is a value returned by localstorage.getItem with the key "forceSVG"', () => { + getItemReturnValue = 'this should not matter'; + const actual = dataSource.getForceSvgFlag(); + expect(getItemSpy).toHaveBeenCalledOnceWith( + TEST_ONLY.FORCE_SVG_RENDERER_KEY + ); + expect(actual).toBeTruthy(); + }); + }); + + describe('updateForceSVG', () => { + it('creates localStorage entry with key forceSVG when passed truthy', () => { + dataSource.updateForceSvgFlag(true); + expect(setItemSpy).toHaveBeenCalledOnceWith( + TEST_ONLY.FORCE_SVG_RENDERER_KEY, + jasmine.any(String) + ); + }); + it('calls localStorage.removeItem with key forceSVG', () => { + dataSource.updateForceSvgFlag(false); + expect(removeItemSpy).toHaveBeenCalledOnceWith( + TEST_ONLY.FORCE_SVG_RENDERER_KEY + ); + }); + }); +}); diff --git a/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts b/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts index 9b2ec299a0a..61e99e29d51 100644 --- a/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts +++ b/tensorboard/webapp/feature_flag/store/feature_flag_selectors.ts @@ -129,3 +129,10 @@ export const getEnabledTimeNamespacedState = createSelector( return flags.enabledTimeNamespacedState; } ); + +export const getForceSvgFeatureFlag = createSelector( + getFeatureFlags, + (flags: FeatureFlags): boolean => { + return flags.forceSvg; + } +); diff --git a/tensorboard/webapp/feature_flag/store/feature_flag_store_config_provider.ts b/tensorboard/webapp/feature_flag/store/feature_flag_store_config_provider.ts index 9818fa28f2b..551d4d2c25b 100644 --- a/tensorboard/webapp/feature_flag/store/feature_flag_store_config_provider.ts +++ b/tensorboard/webapp/feature_flag/store/feature_flag_store_config_provider.ts @@ -32,6 +32,7 @@ export const initialState: FeatureFlagState = { enableTimeSeriesPromotion: false, enabledCardWidthSetting: true, enabledTimeNamespacedState: false, + forceSvg: false, }, flagOverrides: {}, }; diff --git a/tensorboard/webapp/feature_flag/testing.ts b/tensorboard/webapp/feature_flag/testing.ts index 1720fb68326..6ea5bd1d00c 100644 --- a/tensorboard/webapp/feature_flag/testing.ts +++ b/tensorboard/webapp/feature_flag/testing.ts @@ -32,6 +32,7 @@ export function buildFeatureFlag( enableTimeSeriesPromotion: false, enabledCardWidthSetting: false, enabledTimeNamespacedState: false, + forceSvg: false, ...override, }; } diff --git a/tensorboard/webapp/feature_flag/types.ts b/tensorboard/webapp/feature_flag/types.ts index 208643055c0..a98490715a4 100644 --- a/tensorboard/webapp/feature_flag/types.ts +++ b/tensorboard/webapp/feature_flag/types.ts @@ -50,4 +50,7 @@ export interface FeatureFlags { // Whether to enable time-namespaced state and how it impacts how user // settings are kept during navigation. enabledTimeNamespacedState: boolean; + // Flag for the escape hatch from WebGL. This only effects the TimeSeries + // Scalar cards. + forceSvg: boolean; } diff --git a/tensorboard/webapp/metrics/views/card_renderer/BUILD b/tensorboard/webapp/metrics/views/card_renderer/BUILD index cedcd19c4f4..81c8199979e 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/BUILD +++ b/tensorboard/webapp/metrics/views/card_renderer/BUILD @@ -268,6 +268,7 @@ tf_ng_module( "//tensorboard/webapp/angular:expect_angular_material_menu", "//tensorboard/webapp/angular:expect_angular_material_progress_spinner", "//tensorboard/webapp/experiments:types", + "//tensorboard/webapp/feature_flag/store", "//tensorboard/webapp/metrics:types", "//tensorboard/webapp/metrics/data_source", "//tensorboard/webapp/metrics/store", diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html index 8b1c6d89035..8ce3ef97f2a 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html @@ -102,7 +102,7 @@ > { @Input() xAxisType!: XAxisType; @Input() xScaleType!: ScaleType; @Input() useDarkMode!: boolean; + @Input() forceSvg!: boolean; @Input() selectedTime!: ViewSelectedTime | null; @Output() onFullSizeToggle = new EventEmitter(); diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts index a88676a9f0b..90556879280 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts @@ -37,6 +37,7 @@ import { } from 'rxjs/operators'; import {State} from '../../../app_state'; import {ExperimentAlias} from '../../../experiments/types'; +import {getForceSvgFeatureFlag} from '../../../feature_flag/store/feature_flag_selectors'; import { getCardPinnedState, getCurrentRouteRunSelection, @@ -127,6 +128,7 @@ function areSeriesEqual( [xScaleType]="xScaleType$ | async" [useDarkMode]="useDarkMode$ | async" [selectedTime]="selectedTime$ | async" + [forceSvg]="forceSvg$ | async" (onFullSizeToggle)="onFullSizeToggle()" (onPinClicked)="pinStateChanged.emit($event)" observeIntersection @@ -174,6 +176,7 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy { readonly ignoreOutliers$ = this.store.select(getMetricsIgnoreOutliers); readonly tooltipSort$ = this.store.select(getMetricsTooltipSort); readonly xAxisType$ = this.store.select(getMetricsXAxisType); + readonly forceSvg$ = this.store.select(getForceSvgFeatureFlag); readonly xScaleType$ = this.store.select(getMetricsXAxisType).pipe( map((xAxisType) => { switch (xAxisType) { diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts index 288bfa060d9..69941c35f6a 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts @@ -294,6 +294,7 @@ describe('scalar card', () => { ); store.overrideSelector(selectors.getRunColorMap, {}); store.overrideSelector(selectors.getDarkModeEnabled, false); + store.overrideSelector(selectors.getForceSvgFeatureFlag, false); }); describe('basic renders', () => { @@ -500,6 +501,25 @@ describe('scalar card', () => { expect(lineChartEl.componentInstance.useDarkMode).toBe(true); })); + + it('sets preferredRendererType to SVG when getForceSvgFeatureFlag returns true', fakeAsync(() => { + store.overrideSelector(selectors.getForceSvgFeatureFlag, false); + const fixture = createComponent('card1'); + fixture.detectChanges(); + + const lineChartEl = fixture.debugElement.query(Selector.LINE_CHART); + expect(lineChartEl.componentInstance.preferredRendererType).toBe( + RendererType.WEBGL + ); + + store.overrideSelector(selectors.getForceSvgFeatureFlag, true); + store.refreshState(); + fixture.detectChanges(); + + expect(lineChartEl.componentInstance.preferredRendererType).toBe( + RendererType.SVG + ); + })); }); describe('displayName', () => { diff --git a/tensorboard/webapp/webapp_data_source/BUILD b/tensorboard/webapp/webapp_data_source/BUILD index 05a29b639f9..575a40ea7e4 100644 --- a/tensorboard/webapp/webapp_data_source/BUILD +++ b/tensorboard/webapp/webapp_data_source/BUILD @@ -114,6 +114,7 @@ tf_ng_module( deps = [ ":feature_flag_types", "//tensorboard/webapp/feature_flag:testing", + "//tensorboard/webapp/feature_flag:types", "@npm//@angular/core", ], ) diff --git a/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts b/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts index 2f6b313333c..106656d030b 100644 --- a/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts +++ b/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source.ts @@ -23,6 +23,7 @@ import { ENABLE_LINK_TIME_PARAM_KEY, ENABLE_TIME_NAMESPACED_STATE, EXPERIMENTAL_PLUGIN_QUERY_PARAM_KEY, + FORCE_SVG_RENDERER, SCALARS_BATCH_SIZE_PARAM_KEY, TBFeatureFlagDataSource, } from './tb_feature_flag_data_source_types'; @@ -90,6 +91,10 @@ export class QueryParamsFeatureFlagDataSource params.get(ENABLE_TIME_NAMESPACED_STATE) !== 'false'; } + if (params.has(FORCE_SVG_RENDERER)) { + featureFlags.forceSvg = params.get(FORCE_SVG_RENDERER) !== 'false'; + } + return featureFlags; } diff --git a/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_test.ts b/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_test.ts index feed77c6671..3914c0685c6 100644 --- a/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_test.ts +++ b/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_test.ts @@ -183,6 +183,28 @@ describe('tb_feature_flag_data_source', () => { }); }); + it('returns forceSVG from the query params', () => { + getParamsSpy.and.returnValues( + new URLSearchParams('forceSVG=false'), + new URLSearchParams('forceSVG='), + new URLSearchParams('forceSVG=true'), + new URLSearchParams('forceSVG=foo') + ); + + expect(dataSource.getFeatures()).toEqual({ + forceSvg: false, + }); + expect(dataSource.getFeatures()).toEqual({ + forceSvg: true, + }); + expect(dataSource.getFeatures()).toEqual({ + forceSvg: true, + }); + expect(dataSource.getFeatures()).toEqual({ + forceSvg: true, + }); + }); + it('returns all flag values when they are all set', () => { getParamsSpy.and.returnValue( new URLSearchParams( diff --git a/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_types.ts b/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_types.ts index e6db8b43058..b76ed94e5e3 100644 --- a/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_types.ts +++ b/tensorboard/webapp/webapp_data_source/tb_feature_flag_data_source_types.ts @@ -46,3 +46,5 @@ export const ENABLE_DARK_MODE_QUERY_PARAM_KEY = 'darkMode'; export const ENABLE_LINK_TIME_PARAM_KEY = 'enableLinkTime'; export const ENABLE_TIME_NAMESPACED_STATE = 'enableTimeNamespacedState'; + +export const FORCE_SVG_RENDERER = 'forceSVG'; diff --git a/tensorboard/webapp/webapp_data_source/tb_feature_flag_testing.ts b/tensorboard/webapp/webapp_data_source/tb_feature_flag_testing.ts index 5e75c6e5a5d..0f441dc3c3d 100644 --- a/tensorboard/webapp/webapp_data_source/tb_feature_flag_testing.ts +++ b/tensorboard/webapp/webapp_data_source/tb_feature_flag_testing.ts @@ -15,11 +15,12 @@ limitations under the License. import {Injectable, NgModule} from '@angular/core'; import {buildFeatureFlag} from '../feature_flag/testing'; +import {FeatureFlags} from '../feature_flag/types'; import {TBFeatureFlagDataSource} from './tb_feature_flag_data_source_types'; @Injectable() export class TestingTBFeatureFlagDataSource extends TBFeatureFlagDataSource { - getFeatures() { + getFeatures(): Partial { return buildFeatureFlag(); } }