Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions tensorboard/webapp/metrics/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,9 @@ export const metricsClearAllPinnedCards = createAction(
'[Metrics] Clear all pinned cards'
);

export const metricsEnableSavingPinsToggled = createAction(
'[Metrics] Enable Saving Pins Toggled'
);

// TODO(jieweiwu): Delete after internal code is updated.
export const stepSelectorTimeSelectionChanged = timeSelectionChanged;
70 changes: 59 additions & 11 deletions tensorboard/webapp/metrics/effects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,20 @@ export class MetricsEffects implements OnInitEffects {
withLatestFrom(
this.getVisibleCardFetchInfos(),
this.store.select(selectors.getEnableGlobalPins),
this.store.select(selectors.getShouldPersistSettings)
this.store.select(selectors.getShouldPersistSettings),
this.store.select(selectors.getMetricsSavingPinsEnabled)
),
filter(
([, , enableGlobalPins, shouldPersistSettings]) =>
enableGlobalPins && shouldPersistSettings
([
,
,
enableGlobalPinsFeature,
shouldPersistSettings,
isMetricsSavingPinsEnabled,
]) =>
enableGlobalPinsFeature &&
shouldPersistSettings &&
isMetricsSavingPinsEnabled
),
tap(([{cardId, canCreateNewPins, wasPinned}, fetchInfos]) => {
const card = fetchInfos.find((value) => value.id === cardId);
Expand All @@ -293,11 +302,19 @@ export class MetricsEffects implements OnInitEffects {
ofType(initAction),
withLatestFrom(
this.store.select(selectors.getEnableGlobalPins),
this.store.select(selectors.getShouldPersistSettings)
this.store.select(selectors.getShouldPersistSettings),
this.store.select(selectors.getMetricsSavingPinsEnabled)
),
filter(
([, enableGlobalPins, shouldPersistSettings]) =>
enableGlobalPins && shouldPersistSettings
([
,
enableGlobalPinsFeature,
shouldPersistSettings,
isMetricsSavingPinsEnabled,
]) =>
enableGlobalPinsFeature &&
shouldPersistSettings &&
isMetricsSavingPinsEnabled
),
tap(() => {
const tags = this.savedPinsDataSource.getSavedScalarPins();
Expand All @@ -316,15 +333,46 @@ export class MetricsEffects implements OnInitEffects {
})
);

private readonly removeAllPins$ = this.actions$.pipe(
private readonly removeSavedPinsOnDisable$ = this.actions$.pipe(
ofType(actions.metricsClearAllPinnedCards),
withLatestFrom(
this.store.select(selectors.getEnableGlobalPins),
this.store.select(selectors.getShouldPersistSettings)
this.store.select(selectors.getShouldPersistSettings),
this.store.select(selectors.getMetricsSavingPinsEnabled)
),
filter(
([, enableGlobalPins, shouldPersistSettings]) =>
enableGlobalPins && shouldPersistSettings
([
,
enableGlobalPinsFeature,
shouldPersistSettings,
isMetricsSavingPinsEnabled,
]) =>
enableGlobalPinsFeature &&
shouldPersistSettings &&
isMetricsSavingPinsEnabled
),
tap(() => {
this.savedPinsDataSource.removeAllScalarPins();
})
);

private readonly disableSavingPins$ = this.actions$.pipe(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit confused by the naming at first because the effect doesn't actually disable (disabling is done elsewhere), this just removes pins.

WDYT of a naming that describes the actual action, like removeOnDisable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to removeSavedPinsOnDisable 👍

ofType(actions.metricsEnableSavingPinsToggled),
withLatestFrom(
this.store.select(selectors.getEnableGlobalPins),
this.store.select(selectors.getShouldPersistSettings),
this.store.select(selectors.getMetricsSavingPinsEnabled)
),
filter(
([
,
enableGlobalPins,
getShouldPersistSettings,
getMetricsSavingPinsEnabled,
]) =>
enableGlobalPins &&
getShouldPersistSettings &&
!getMetricsSavingPinsEnabled
),
tap(() => {
this.savedPinsDataSource.removeAllScalarPins();
Expand Down Expand Up @@ -375,7 +423,7 @@ export class MetricsEffects implements OnInitEffects {
/**
* Subscribes to: metricsClearAllPinnedCards.
*/
this.removeAllPins$
this.removeSavedPinsOnDisable$
);
},
{dispatch: false}
Expand Down
10 changes: 10 additions & 0 deletions tensorboard/webapp/metrics/metrics_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
getMetricsScalarSmoothing,
getMetricsStepSelectorEnabled,
getMetricsTooltipSort,
getMetricsSavingPinsEnabled,
getRangeSelectionHeaders,
getSingleSelectionHeaders,
isMetricsSettingsPaneOpen,
Expand Down Expand Up @@ -125,6 +126,12 @@ export function getMetricsTimeSeriesLinkedTimeEnabled() {
});
}

export function getMetricsTimeSeriesSavingPinsEnabled() {
return createSelector(getMetricsSavingPinsEnabled, (isEnabled) => {
return {savingPinsEnabled: isEnabled};
});
}

export function getSingleSelectionHeadersFactory() {
return createSelector(getSingleSelectionHeaders, (singleSelectionHeaders) => {
return {singleSelectionHeaders};
Expand Down Expand Up @@ -188,6 +195,9 @@ export function getRangeSelectionHeadersFactory() {
PersistentSettingsConfigModule.defineGlobalSetting(
getRangeSelectionHeadersFactory
),
PersistentSettingsConfigModule.defineGlobalSetting(
getMetricsTimeSeriesSavingPinsEnabled
),
],
providers: [
{
Expand Down
16 changes: 16 additions & 0 deletions tensorboard/webapp/metrics/store/metrics_reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@ const reducer = createReducer(
if (typeof partialSettings.scalarSmoothing === 'number') {
metricsSettings.scalarSmoothing = partialSettings.scalarSmoothing;
}
if (typeof partialSettings.savingPinsEnabled === 'boolean') {
metricsSettings.savingPinsEnabled = partialSettings.savingPinsEnabled;
}

const isSettingsPaneOpen =
partialSettings.timeSeriesSettingsPaneOpened ?? state.isSettingsPaneOpen;
Expand Down Expand Up @@ -933,6 +936,19 @@ const reducer = createReducer(
},
};
}),
on(actions.metricsEnableSavingPinsToggled, (state) => {
const nextSavingPinsEnabled = !(
state.settingOverrides.savingPinsEnabled ??
state.settings.savingPinsEnabled
Copy link
Member

@hoonji hoonji Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[optional] I'm curious, did you find out anything about the "legacy reasons" why we need settings as well as settingsOverrides?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be mistaken, but based on my understanding, the settings property is used for setting default values and loading persistent settings, while the property settingsOverrides is used when a user makes changes, such as toggling a checkbox.

In the code, the settingsOverrides is used to override the original setting store value. I'm not sure why these properties are separated, but that's what I discovered while examining the code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that makes it clearer!

);
return {
...state,
settingOverrides: {
...state.settingOverrides,
savingPinsEnabled: nextSavingPinsEnabled,
},
};
}),
on(
actions.multipleTimeSeriesRequested,
(
Expand Down
22 changes: 22 additions & 0 deletions tensorboard/webapp/metrics/store/metrics_reducers_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,25 @@ describe('metrics reducers', () => {
expect(thirdState.settings.hideEmptyCards).toBe(false);
expect(thirdState.settingOverrides.hideEmptyCards).toBe(false);
});

it('changes savingPinsEnabled on metricsEnableSavingPinsToggled', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this tests a "toggle", could we have at least two test cases minimum here? One changing from true -> false, another from false -> true (parameterized tests may help - please see other comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some states to make it true -> false -> true.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: interleaving act and assert steps is explicitly forbidden by our style guide and I think we should follow it unless there's a very strong reason not to. Can we just create separate tests for true->false and false->true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. Thanks for reminding me! I just followed this interleaving act and assert steps because it was commonly used in this test codebase

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the cleanup! My readability meta-mentor linked me to this when I asked if consistency is important: https://peps.python.org/pep-0008/#a-foolish-consistency-is-the-hobgoblin-of-little-minds

My stance now is the right thing > consistency, unless inconsistency causes big problems

[{value: true}, {value: false}].forEach(({value: initValue}) => {
const prevState = buildMetricsState({
settings: buildMetricsSettingsState({
savingPinsEnabled: initValue,
}),
settingOverrides: {},
});

const nextState = reducers(
prevState,
actions.metricsEnableSavingPinsToggled()
);

expect(nextState.settings.savingPinsEnabled).toBe(initValue);
expect(nextState.settingOverrides.savingPinsEnabled).toBe(!initValue);
});
});
});

describe('loading time series data', () => {
Expand Down Expand Up @@ -3142,6 +3161,7 @@ describe('metrics reducers', () => {
scalarSmoothing: 0.3,
ignoreOutliers: false,
tooltipSort: TooltipSort.ASCENDING,
savingPinsEnabled: true,
}),
settingOverrides: {
scalarSmoothing: 0.5,
Expand All @@ -3155,13 +3175,15 @@ describe('metrics reducers', () => {
partialSettings: {
ignoreOutliers: true,
tooltipSort: TooltipSort.DESCENDING,
savingPinsEnabled: false,
},
})
);

expect(nextState.settings.scalarSmoothing).toBe(0.3);
expect(nextState.settings.ignoreOutliers).toBe(true);
expect(nextState.settings.tooltipSort).toBe(TooltipSort.DESCENDING);
expect(nextState.settings.savingPinsEnabled).toBe(false);
expect(nextState.settingOverrides.scalarSmoothing).toBe(0.5);
expect(nextState.settingOverrides.tooltipSort).toBe(
TooltipSort.ALPHABETICAL
Expand Down
5 changes: 5 additions & 0 deletions tensorboard/webapp/metrics/store/metrics_selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,11 @@ export const getMetricsImageShowActualSize = createSelector(
(settings): boolean => settings.imageShowActualSize
);

export const getMetricsSavingPinsEnabled = createSelector(
selectSettings,
(settings): boolean => settings.savingPinsEnabled
);

export const getMetricsTagFilter = createSelector(
selectMetricsState,
(state): string => state.tagFilter
Expand Down
14 changes: 14 additions & 0 deletions tensorboard/webapp/metrics/store/metrics_selectors_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,20 @@ describe('metrics selectors', () => {
);
expect(selectors.getMetricsCardMinWidth(state)).toBe(400);
});

it('returns savingPinsEnabled when called getMetricsSavingPinsEnabled', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the test isn't very robust, as many different expressions could evaluate to false. A easy, succinct way to test both true/false can be parameterized testing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

[{value: true}, {value: false}].forEach(({value}) => {
selectors.getMetricsSavingPinsEnabled.release();
const state = appStateFromMetricsState(
buildMetricsState({
settings: buildMetricsSettingsState({
savingPinsEnabled: value,
}),
})
);
expect(selectors.getMetricsSavingPinsEnabled(state)).toBe(value);
});
});
});

describe('getMetricsTagFilter', () => {
Expand Down
2 changes: 2 additions & 0 deletions tensorboard/webapp/metrics/store/metrics_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ export interface MetricsSettings {
imageContrastInMilli: number;
imageShowActualSize: boolean;
histogramMode: HistogramMode;
savingPinsEnabled: boolean;
}

export interface MetricsNonNamespacedState {
Expand Down Expand Up @@ -287,4 +288,5 @@ export const METRICS_SETTINGS_DEFAULT: MetricsSettings = {
imageContrastInMilli: 1000,
imageShowActualSize: false,
histogramMode: HistogramMode.OFFSET,
savingPinsEnabled: true,
};
22 changes: 22 additions & 0 deletions tensorboard/webapp/metrics/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ import {DataTableMode} from '../widgets/data_table/types';
export function buildMetricsSettingsState(
overrides?: Partial<MetricsSettings>
): MetricsSettings {
return {
cardMinWidth: null,
tooltipSort: TooltipSort.NEAREST,
ignoreOutliers: false,
xAxisType: XAxisType.WALL_TIME,
scalarSmoothing: 0.3,
hideEmptyCards: true,
scalarPartitionNonMonotonicX: false,
imageBrightnessInMilli: 123,
imageContrastInMilli: 123,
imageShowActualSize: true,
histogramMode: HistogramMode.OFFSET,
savingPinsEnabled: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change in MetricsSettings required tbcorp change so I created cl/625208577

...overrides,
};
}

// Since Settings proto has missing fields, we need to build a partial of
// Settings to be used in tests.
export function buildMetricsSettingsOverrides(
overrides?: Partial<MetricsSettings>
): Partial<MetricsSettings> {
return {
cardMinWidth: null,
tooltipSort: TooltipSort.NEAREST,
Expand Down
38 changes: 38 additions & 0 deletions tensorboard/webapp/metrics/views/right_pane/right_pane_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,5 +548,43 @@ describe('metrics right_pane', () => {
).toHaveClass('toggle-opened');
});
});

describe('saving pins check box', () => {
beforeEach(() => {
store.overrideSelector(selectors.getEnableGlobalPins, true);
store.overrideSelector(selectors.getMetricsSavingPinsEnabled, false);
});

it('does not render if getEnableGlobalPins feature flag is false', () => {
store.overrideSelector(selectors.getEnableGlobalPins, false);
const fixture = TestBed.createComponent(SettingsViewContainer);
fixture.detectChanges();

expect(select(fixture, '.saving-pins')).toBeFalsy();
});

it('renders checked saving pins check box if isSavingpinsEnabled is true', () => {
store.overrideSelector(selectors.getMetricsSavingPinsEnabled, true);

const fixture = TestBed.createComponent(SettingsViewContainer);
fixture.detectChanges();

expect(
select(fixture, '.saving-pins input').componentInstance.checked
).toBeTrue();
});

it('dispatches metricsEnableSavingPinsToggled on toggle', () => {
const fixture = TestBed.createComponent(SettingsViewContainer);
fixture.detectChanges();

const checkbox = select(fixture, '.saving-pins input');
checkbox.nativeElement.click();

expect(dispatchSpy).toHaveBeenCalledWith(
actions.metricsEnableSavingPinsToggled()
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ <h3 class="section-title">General</h3>
</button>
</div>
</div>

<div class="control-row saving-pins" *ngIf="globalPinsFeatureEnabled">
<mat-checkbox
[checked]="isSavingPinsEnabled"
(change)="onEnableSavingPinsToggled.emit()"
>Enable saving pins (Scalars only)</mat-checkbox
>
<mat-icon
class="info"
svgIcon="help_outline_24px"
title="When saving pins are enabled, pinned cards will be visible across multiple experiments."
></mat-icon>
</div>
</section>

<section class="scalars">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ section .control-row:not(:has(+ .control-row > mat-checkbox)):not(:last-child) {
width: 5em;
}

.scalars-partition-x {
.scalars-partition-x,
.saving-pins {
align-items: center;
display: flex;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,15 @@ export class SettingsViewComponent {
@Input() linkedTimeSelection!: TimeSelection | null;
@Input() stepMinMax!: {min: number; max: number};
@Input() isSlideOutMenuOpen!: boolean;
@Input() isSavingPinsEnabled!: boolean;
@Input() globalPinsFeatureEnabled: boolean = false;

@Output() linkedTimeToggled = new EventEmitter<void>();

@Output() stepSelectorToggled = new EventEmitter<void>();
@Output() rangeSelectionToggled = new EventEmitter<void>();
@Output() onSlideOutToggled = new EventEmitter<void>();
@Output() onEnableSavingPinsToggled = new EventEmitter<void>();

@Input() isImageSupportEnabled!: boolean;

Expand Down
Loading