From 9eb247ce5bef9670d63736642ba70df55ecfedce Mon Sep 17 00:00:00 2001 From: Riley Jones <78179109+rileyajones@users.noreply.github.com> Date: Tue, 14 Mar 2023 11:40:21 -0700 Subject: [PATCH] Allow card step selection to be slightly independent of global step selection in redux (#6230) ## Motivation for features / changes #6172 began the effort of moving the step selector to redux, however, cards are now able to operate with some amount of independence from the global state. To resolve this while also keeping things global, I have added a handful of additional settings which can be used to determine the correct time and range selection. ## Technical description of changes Three state enums allow for a cleaner description of the state than would be possible with booleans (I did try both). This also allows clicking the checkboxes in the settings panel to ensure the correct overrides are set. ### `stepSelection` This detaches the existence of a cards step selection in redux state from the need to display that step selection. This also allows step selection to be disabled for individual cards while remaining globally enabled. * When a cards step selection is modified, its `stepSelection` setting is set to `ENABLED` * When all steps of a card are removed, the cards `stepSelection` setting is set to `DISABLED` ### `rangeSelection` Similarly to `stepSelection` this enables a card to have an end value without needing to display it. * When a cards time selection is modified, if an end step is being set, `rangeSelection` is set to `ENABLED`, otherwise, `DISABLED`. ## Screenshots of UI changes No changes ## Detailed steps to verify changes work correctly (as executed by you) * I wrote a lot of tests * I also forked this PR and made changes to the scalar card container to allow me to play around with it. This branch on my fork has the necessary changes https://github.com/rileyajones/tensorboard/tree/step-selector-redux-4 ## Alternate designs / implementations considered While the card specific settings should take precedent over the global settings, the global controls (the settings checkboxes) should override those settings. This means that it is necessary to remove overrides when they are clicked. --- tensorboard/webapp/metrics/actions/index.ts | 7 + .../webapp/metrics/store/metrics_reducers.ts | 60 ++++- .../metrics/store/metrics_reducers_test.ts | 107 +++++++- .../webapp/metrics/store/metrics_selectors.ts | 50 +++- .../metrics/store/metrics_selectors_test.ts | 252 +++++++++++++++--- .../store/metrics_store_internal_utils.ts | 15 ++ .../metrics_store_internal_utils_test.ts | 53 +++- .../webapp/metrics/store/metrics_types.ts | 8 + 8 files changed, 510 insertions(+), 42 deletions(-) diff --git a/tensorboard/webapp/metrics/actions/index.ts b/tensorboard/webapp/metrics/actions/index.ts index f995cfc94f..05c34f1d15 100644 --- a/tensorboard/webapp/metrics/actions/index.ts +++ b/tensorboard/webapp/metrics/actions/index.ts @@ -244,6 +244,13 @@ export const stepSelectorToggled = createAction( // Affordance for internal analytics purpose. When no affordance is specified or is // undefined we do not want to log an analytics event. affordance?: TimeSelectionToggleAffordance; + // This action can be triggered by two different events: + // 1) Clicking the checkbox in the settings panel + // 2) Removing the last fob from a scalar card + // + // Setting the cardId results in stepSelection being toggled for a specific card. + // Without the cardId being set this action only effects the global stepSeletion. + cardId?: CardId; }>() ); export const rangeSelectionToggled = createAction( diff --git a/tensorboard/webapp/metrics/store/metrics_reducers.ts b/tensorboard/webapp/metrics/store/metrics_reducers.ts index 94729a83fe..c8ce4ab451 100644 --- a/tensorboard/webapp/metrics/store/metrics_reducers.ts +++ b/tensorboard/webapp/metrics/store/metrics_reducers.ts @@ -66,7 +66,9 @@ import { getTimeSeriesLoadable, } from './metrics_store_internal_utils'; import { + CardFeatureOverride, CardMetadataMap, + CardStateMap, CardStepIndexMap, MetricsNamespacedState, MetricsNonNamespacedState, @@ -1050,6 +1052,25 @@ const reducer = createReducer( let nextStepSelectorEnabled = state.stepSelectorEnabled; let linkedTimeSelection = state.linkedTimeSelection; + const nextCardStateMap = Object.entries(state.cardStateMap).reduce( + (cardStateMap, [cardId, cardState]) => { + // Range selection is tiered, it can be turned on/off globally and + // then overridden for an individual card. + // + // Since range selection was last toggled on/off, some cards were + // individually turned off/on respectively. Those cards differed + // from the "global" step selection enablement state. Now that + // range selection is being turned back on or off, all cards once + // again have the "global" state. + cardStateMap[cardId] = { + ...cardState, + rangeSelectionOverride: CardFeatureOverride.NONE, + }; + return cardStateMap; + }, + {} as CardStateMap + ); + if (nextRangeSelectionEnabled) { nextStepSelectorEnabled = nextRangeSelectionEnabled; if (!linkedTimeSelection) { @@ -1077,6 +1098,7 @@ const reducer = createReducer( stepSelectorEnabled: nextStepSelectorEnabled, rangeSelectionEnabled: nextRangeSelectionEnabled, linkedTimeSelection, + cardStateMap: nextCardStateMap, }; }), on(actions.timeSelectionChanged, (state, change) => { @@ -1113,6 +1135,11 @@ const reducer = createReducer( nextCardStateMap[cardId] = { ...nextCardStateMap[cardId], timeSelection: nextTimeSelection, + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + rangeSelectionOverride: + nextTimeSelection.end?.step === undefined + ? CardFeatureOverride.OVERRIDE_AS_DISABLED + : CardFeatureOverride.OVERRIDE_AS_ENABLED, }; } @@ -1136,14 +1163,42 @@ const reducer = createReducer( cardStateMap: nextCardStateMap, }; }), - on(actions.stepSelectorToggled, (state, {affordance}) => { + on(actions.stepSelectorToggled, (state, {affordance, cardId}) => { + const nextCardStateMap = {...state.cardStateMap}; + if (cardId) { + // cardId is only included when the event is generated from a scalar card + // The only time that the scalar card dispatches a step selection toggled + // event is when the last fob is being removed, therefore this should + // always result in stepSelection being disabled. + const {timeSelection, ...cardState} = nextCardStateMap[cardId] || {}; + nextCardStateMap[cardId] = { + ...cardState, + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_DISABLED, + }; + } else { + // Step selection is tiered, it can be turned on/off global and then + // overridden for an individual card. + // + // When no cardId is provided, the global status is being changed and + // thus all cards should be made to adhere to the new state. + Object.keys(nextCardStateMap).forEach((cardId) => { + nextCardStateMap[cardId] = { + ...nextCardStateMap[cardId], + stepSelectionOverride: CardFeatureOverride.NONE, + }; + }); + } + if ( !state.linkedTimeEnabled && affordance !== TimeSelectionToggleAffordance.CHECK_BOX ) { // In plain step selection mode (without linked time), we do not allow // interactions with fobs to modify global step selection state. - return {...state}; + return { + ...state, + cardStateMap: nextCardStateMap, + }; } const nextStepSelectorEnabled = !state.stepSelectorEnabled; @@ -1157,6 +1212,7 @@ const reducer = createReducer( linkedTimeEnabled: nextLinkedTimeEnabled, stepSelectorEnabled: nextStepSelectorEnabled, rangeSelectionEnabled: nextRangeSelectionEnabled, + cardStateMap: nextCardStateMap, }; }), on(actions.timeSelectionCleared, (state) => { diff --git a/tensorboard/webapp/metrics/store/metrics_reducers_test.ts b/tensorboard/webapp/metrics/store/metrics_reducers_test.ts index 199275f298..b0567bfda7 100644 --- a/tensorboard/webapp/metrics/store/metrics_reducers_test.ts +++ b/tensorboard/webapp/metrics/store/metrics_reducers_test.ts @@ -56,6 +56,7 @@ import { import {reducers} from './metrics_reducers'; import {getCardId, getPinnedCardId} from './metrics_store_internal_utils'; import { + CardFeatureOverride, CardMetadataMap, MetricsState, RunToLoadState, @@ -3068,7 +3069,7 @@ describe('metrics reducers', () => { cardId: 'card2', timeSelection: { start: {step: 1}, - end: {step: 5}, + end: null, }, }) ); @@ -3078,8 +3079,10 @@ describe('metrics reducers', () => { card2: { timeSelection: { start: {step: 1}, - end: {step: 5}, + end: null, }, + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + rangeSelectionOverride: CardFeatureOverride.OVERRIDE_AS_DISABLED, }, }); }); @@ -3121,6 +3124,38 @@ describe('metrics reducers', () => { start: {step: 1}, end: {step: 5}, }, + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + rangeSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + }, + }); + }); + + it('enables card specific range selection if an end value is provided', () => { + const state1 = buildMetricsState({ + cardStateMap: { + card1: {}, + }, + }); + const state2 = reducers( + state1, + actions.timeSelectionChanged({ + cardId: 'card2', + timeSelection: { + start: {step: 1}, + end: {step: 5}, + }, + }) + ); + + expect(state2.cardStateMap).toEqual({ + card1: {}, + card2: { + timeSelection: { + start: {step: 1}, + end: {step: 5}, + }, + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + rangeSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, }, }); }); @@ -3233,6 +3268,37 @@ describe('metrics reducers', () => { end: null, }); }); + + it('sets all card specific overrides to default', () => { + const state1 = buildMetricsState({ + linkedTimeSelection: { + start: {step: 100}, + end: {step: 1000}, + }, + rangeSelectionEnabled: false, + cardStateMap: { + card1: { + rangeSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + }, + card2: { + rangeSelectionOverride: CardFeatureOverride.OVERRIDE_AS_DISABLED, + }, + card3: {}, + }, + }); + const state2 = reducers(state1, actions.rangeSelectionToggled({})); + expect(state2.cardStateMap).toEqual({ + card1: { + rangeSelectionOverride: CardFeatureOverride.NONE, + }, + card2: { + rangeSelectionOverride: CardFeatureOverride.NONE, + }, + card3: { + rangeSelectionOverride: CardFeatureOverride.NONE, + }, + }); + }); }); describe('#cardMinMaxChanged', () => { @@ -3549,6 +3615,43 @@ describe('metrics reducers', () => { expect(state2.linkedTimeEnabled).toBe(false); expect(state2.rangeSelectionEnabled).toBe(false); }); + + it('disables card specific step selection when cardId is provided', () => { + const prevState = buildMetricsState(); + const nextState = reducers( + prevState, + actions.stepSelectorToggled({cardId: 'card1'}) + ); + expect(nextState.cardStateMap['card1'].stepSelectionOverride).toEqual( + CardFeatureOverride.OVERRIDE_AS_DISABLED + ); + }); + + it('removes all card specific overrides when no card id is provided', () => { + const prevState = buildMetricsState({ + cardStateMap: { + card1: { + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + }, + card2: { + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_DISABLED, + }, + card3: {}, + }, + }); + const nextState = reducers(prevState, actions.stepSelectorToggled({})); + expect(nextState.cardStateMap).toEqual({ + card1: { + stepSelectionOverride: CardFeatureOverride.NONE, + }, + card2: { + stepSelectionOverride: CardFeatureOverride.NONE, + }, + card3: { + stepSelectionOverride: CardFeatureOverride.NONE, + }, + }); + }); }); describe('plugin filtering feature', () => { diff --git a/tensorboard/webapp/metrics/store/metrics_selectors.ts b/tensorboard/webapp/metrics/store/metrics_selectors.ts index e475a18023..782b1a9d40 100644 --- a/tensorboard/webapp/metrics/store/metrics_selectors.ts +++ b/tensorboard/webapp/metrics/store/metrics_selectors.ts @@ -34,7 +34,10 @@ import { MinMaxStep, } from '../views/card_renderer/scalar_card_types'; import * as storeUtils from './metrics_store_internal_utils'; -import {getMinMaxStepFromCardState} from './metrics_store_internal_utils'; +import { + getCardSelectionStateToBoolean, + getMinMaxStepFromCardState, +} from './metrics_store_internal_utils'; import { CardMetadataMap, CardStateMap, @@ -391,6 +394,22 @@ export const getMetricsRangeSelectionEnabled = createSelector( } ); +export const getMetricsCardRangeSelectionEnabled = createSelector( + getMetricsRangeSelectionEnabled, + getCardStateMap, + ( + globalRangeSelectionEnabled: boolean, + cardStateMap: CardStateMap, + cardId: CardId + ) => { + const cardState = cardStateMap[cardId]; + return getCardSelectionStateToBoolean( + cardState?.rangeSelectionOverride, + globalRangeSelectionEnabled + ); + } +); + export const getMetricsStepMinMax = createSelector( selectMetricsState, (state: MetricsState): {min: number; max: number} => { @@ -487,7 +506,6 @@ export const getMetricsCardMinMax = createSelector( getCardStateMap, (cardStateMap: CardStateMap, cardId: CardId): MinMaxStep | undefined => { if (!cardStateMap[cardId]) return; - return getMinMaxStepFromCardState(cardStateMap[cardId]); } ); @@ -497,29 +515,47 @@ export const getMetricsCardMinMax = createSelector( */ export const getMetricsCardTimeSelection = createSelector( getCardStateMap, + getMetricsStepSelectorEnabled, getMetricsLinkedTimeEnabled, getMetricsLinkedTimeSelection, ( cardStateMap: CardStateMap, + globalStepSelectionEnabled: boolean, linkedTimeEnabled: boolean, linkedTimeSelection: TimeSelection | null, cardId: CardId ): TimeSelection | undefined => { + // Handling Linked Time if (linkedTimeEnabled && linkedTimeSelection) { return linkedTimeSelection; } - if (cardStateMap[cardId]?.timeSelection) { - return cardStateMap[cardId]?.timeSelection; + const cardState = cardStateMap[cardId]; + if (!cardState) { + return; } - const minMaxStep = getMinMaxStepFromCardState(cardStateMap[cardId]); + const minMaxStep = getMinMaxStepFromCardState(cardState); if (!minMaxStep) { return; } + // If the user has disabled step selection, nothing should be returned. + if ( + !getCardSelectionStateToBoolean( + cardState.stepSelectionOverride, + globalStepSelectionEnabled + ) + ) { + return; + } + + const startStep = cardState.timeSelection?.start.step ?? minMaxStep.minStep; + const endStep = cardState.timeSelection?.end?.step ?? minMaxStep.maxStep; + + // The default time selection return { - start: {step: minMaxStep.minStep}, - end: {step: minMaxStep.maxStep}, + start: {step: startStep}, + end: {step: endStep}, }; } ); diff --git a/tensorboard/webapp/metrics/store/metrics_selectors_test.ts b/tensorboard/webapp/metrics/store/metrics_selectors_test.ts index 55be8b2652..03c4c4bc1d 100644 --- a/tensorboard/webapp/metrics/store/metrics_selectors_test.ts +++ b/tensorboard/webapp/metrics/store/metrics_selectors_test.ts @@ -27,6 +27,7 @@ import { } from '../testing'; import {HistogramMode, TooltipSort, XAxisType} from '../types'; import * as selectors from './metrics_selectors'; +import {CardFeatureOverride, MetricsState} from './metrics_types'; describe('metrics selectors', () => { beforeEach(() => { @@ -425,45 +426,173 @@ describe('metrics selectors', () => { }); describe('getMetricsCardTimeSelection', () => { - it('returns cards timeSelection if defined', () => { - const state = appStateFromMetricsState( - buildMetricsState({ + describe('when linked time is disabled', () => { + let partialState: Partial; + beforeEach(() => { + partialState = { linkedTimeEnabled: false, - cardStateMap: { - card1: { - timeSelection: { - start: {step: 0}, - end: {step: 5}, + }; + }); + + it('returns cards timeSelection if defined', () => { + const state = appStateFromMetricsState( + buildMetricsState({ + ...partialState, + cardStateMap: { + card1: { + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + dataMinMax: { + minStep: 0, + maxStep: 10, + }, + timeSelection: { + start: {step: 0}, + end: {step: 5}, + }, }, }, - }, - }) - ); + }) + ); - expect(selectors.getMetricsCardTimeSelection(state, 'card1')).toEqual({ - start: {step: 0}, - end: {step: 5}, + expect(selectors.getMetricsCardTimeSelection(state, 'card1')).toEqual({ + start: {step: 0}, + end: {step: 5}, + }); }); - }); - it('returns cards minMax as a timeSelection if timeSelection is undefined', () => { - const state = appStateFromMetricsState( - buildMetricsState({ - linkedTimeEnabled: false, - cardStateMap: { - card1: { - dataMinMax: { - minStep: 0, - maxStep: 5, + it('returns undefined if step selection is disabled', () => { + expect( + selectors.getMetricsCardTimeSelection( + appStateFromMetricsState( + buildMetricsState({ + ...partialState, + stepSelectorEnabled: true, + cardStateMap: { + card1: { + stepSelectionOverride: + CardFeatureOverride.OVERRIDE_AS_DISABLED, + dataMinMax: { + minStep: 0, + maxStep: 10, + }, + timeSelection: { + start: {step: 0}, + end: {step: 5}, + }, + }, + }, + }) + ), + 'card1' + ) + ).toBeUndefined(); + + expect( + selectors.getMetricsCardTimeSelection( + appStateFromMetricsState( + buildMetricsState({ + ...partialState, + stepSelectorEnabled: false, + cardStateMap: { + card1: { + dataMinMax: { + minStep: 0, + maxStep: 10, + }, + timeSelection: { + start: {step: 0}, + end: {step: 5}, + }, + }, + }, + }) + ), + 'card1' + ) + ).toBeUndefined(); + }); + + it('returns undefined if no min max is defined', () => { + expect( + selectors.getMetricsCardTimeSelection( + appStateFromMetricsState( + buildMetricsState({ + ...partialState, + cardStateMap: { + card1: { + timeSelection: { + start: {step: 0}, + end: {step: 5}, + }, + }, + }, + }) + ), + 'card1' + ) + ).toBeUndefined(); + }); + + it('returns undefined when there is no card state', () => { + expect( + selectors.getMetricsCardTimeSelection( + appStateFromMetricsState( + buildMetricsState({ + ...partialState, + cardStateMap: {}, + }) + ), + 'card1' + ) + ).toBeUndefined(); + }); + + it('uses max step as end value if none exists', () => { + const state = appStateFromMetricsState( + buildMetricsState({ + ...partialState, + cardStateMap: { + card1: { + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + dataMinMax: { + minStep: 0, + maxStep: 10, + }, + timeSelection: { + start: {step: 0}, + end: null, + }, }, }, - }, - }) - ); + }) + ); - expect(selectors.getMetricsCardTimeSelection(state, 'card1')).toEqual({ - start: {step: 0}, - end: {step: 5}, + expect(selectors.getMetricsCardTimeSelection(state, 'card1')).toEqual({ + start: {step: 0}, + end: {step: 10}, + }); + }); + + it('returns cards minMax as a timeSelection if timeSelection is undefined', () => { + const state = appStateFromMetricsState( + buildMetricsState({ + ...partialState, + cardStateMap: { + card1: { + stepSelectionOverride: CardFeatureOverride.OVERRIDE_AS_ENABLED, + dataMinMax: { + minStep: 0, + maxStep: 5, + }, + }, + }, + }) + ); + + expect(selectors.getMetricsCardTimeSelection(state, 'card1')).toEqual({ + start: {step: 0}, + end: {step: 5}, + }); }); }); @@ -877,6 +1006,69 @@ describe('metrics selectors', () => { }); }); + describe('getMetricsCardRangeSelectionEnabled', () => { + it('returns card specific value when defined', () => { + expect( + selectors.getMetricsCardRangeSelectionEnabled( + appStateFromMetricsState( + buildMetricsState({ + rangeSelectionEnabled: false, + cardStateMap: { + card1: { + rangeSelectionOverride: + CardFeatureOverride.OVERRIDE_AS_ENABLED, + }, + }, + }) + ), + 'card1' + ) + ).toBeTrue(); + expect( + selectors.getMetricsCardRangeSelectionEnabled( + appStateFromMetricsState( + buildMetricsState({ + rangeSelectionEnabled: true, + cardStateMap: { + card1: { + rangeSelectionOverride: + CardFeatureOverride.OVERRIDE_AS_DISABLED, + }, + }, + }) + ), + 'card1' + ) + ).toBeFalse(); + }); + + it('returns global value when card specific value is not defined', () => { + expect( + selectors.getMetricsCardRangeSelectionEnabled( + appStateFromMetricsState( + buildMetricsState({ + rangeSelectionEnabled: true, + cardStateMap: { + card1: {}, + }, + }) + ), + 'card1' + ) + ).toBeTrue(); + expect( + selectors.getMetricsCardRangeSelectionEnabled( + appStateFromMetricsState( + buildMetricsState({ + rangeSelectionEnabled: false, + }) + ), + 'card1' + ) + ).toBeFalse(); + }); + }); + describe('getMetricsStepMinMax', () => { beforeEach(() => { selectors.getMetricsStepMinMax.release(); diff --git a/tensorboard/webapp/metrics/store/metrics_store_internal_utils.ts b/tensorboard/webapp/metrics/store/metrics_store_internal_utils.ts index 0be4ee6ad5..fac8be44d2 100644 --- a/tensorboard/webapp/metrics/store/metrics_store_internal_utils.ts +++ b/tensorboard/webapp/metrics/store/metrics_store_internal_utils.ts @@ -27,6 +27,7 @@ import { TimeSelection, } from '../types'; import { + CardFeatureOverride, CardMetadataMap, CardState, CardStateMap, @@ -581,6 +582,20 @@ export function getMinMaxStepFromCardState(cardState: Partial) { return userMinMax || dataMinMax; } +export function getCardSelectionStateToBoolean( + cardOverrideState: CardFeatureOverride | undefined, + globalValue: boolean +) { + switch (cardOverrideState) { + case CardFeatureOverride.OVERRIDE_AS_ENABLED: + return true; + case CardFeatureOverride.OVERRIDE_AS_DISABLED: + return false; + default: + return globalValue; + } +} + export const TEST_ONLY = { getImageCardSteps, getSelectedSteps, diff --git a/tensorboard/webapp/metrics/store/metrics_store_internal_utils_test.ts b/tensorboard/webapp/metrics/store/metrics_store_internal_utils_test.ts index d0b90089ee..cd79b197bd 100644 --- a/tensorboard/webapp/metrics/store/metrics_store_internal_utils_test.ts +++ b/tensorboard/webapp/metrics/store/metrics_store_internal_utils_test.ts @@ -31,13 +31,18 @@ import { generateNextPinnedCardMappings, generateScalarCardMinMaxStep, getCardId, + getCardSelectionStateToBoolean, getMinMaxStepFromCardState, getPinnedCardId, getRunIds, getTimeSeriesLoadable, TEST_ONLY, } from './metrics_store_internal_utils'; -import {ImageTimeSeriesData, TimeSeriesData} from './metrics_types'; +import { + CardFeatureOverride, + ImageTimeSeriesData, + TimeSeriesData, +} from './metrics_types'; const { getImageCardSteps, @@ -1191,4 +1196,50 @@ describe('metrics store utils', () => { }); }); }); + + describe('getCardSelectionStateToBoolean', () => { + it('returns true when selection state is ENABLED', () => { + expect( + getCardSelectionStateToBoolean( + CardFeatureOverride.OVERRIDE_AS_ENABLED, + false + ) + ).toBeTrue(); + expect( + getCardSelectionStateToBoolean( + CardFeatureOverride.OVERRIDE_AS_ENABLED, + true + ) + ).toBeTrue(); + }); + + it('returns false when selection state is DISABLED', () => { + expect( + getCardSelectionStateToBoolean( + CardFeatureOverride.OVERRIDE_AS_DISABLED, + true + ) + ).toBeFalse(); + expect( + getCardSelectionStateToBoolean( + CardFeatureOverride.OVERRIDE_AS_DISABLED, + false + ) + ).toBeFalse(); + }); + + it('returns global value when selection state is GLOBAL', () => { + expect( + getCardSelectionStateToBoolean(CardFeatureOverride.NONE, true) + ).toBeTrue(); + expect( + getCardSelectionStateToBoolean(CardFeatureOverride.NONE, false) + ).toBeFalse(); + }); + + it('returns global value when selection state is undefined', () => { + expect(getCardSelectionStateToBoolean(undefined, true)).toBeTrue(); + expect(getCardSelectionStateToBoolean(undefined, false)).toBeFalse(); + }); + }); }); diff --git a/tensorboard/webapp/metrics/store/metrics_types.ts b/tensorboard/webapp/metrics/store/metrics_types.ts index 42b7925d3d..8753aded84 100644 --- a/tensorboard/webapp/metrics/store/metrics_types.ts +++ b/tensorboard/webapp/metrics/store/metrics_types.ts @@ -126,10 +126,18 @@ export type CardMetadataMap = Record< CardMetadata >; +export enum CardFeatureOverride { + NONE, + OVERRIDE_AS_ENABLED, + OVERRIDE_AS_DISABLED, +} + export type CardState = { dataMinMax: MinMaxStep; userMinMax: MinMaxStep; timeSelection: TimeSelection; + stepSelectionOverride: CardFeatureOverride; + rangeSelectionOverride: CardFeatureOverride; tableExpanded: boolean; };