From 57c6a47652dd45b2403a8d8e77618c8d9a42c31e Mon Sep 17 00:00:00 2001 From: Seayeon Lee Date: Thu, 11 Apr 2024 08:44:45 +0000 Subject: [PATCH 1/7] add metricsClearAllPinnedCards action and related reducer --- tensorboard/webapp/metrics/actions/index.ts | 4 ++ .../webapp/metrics/store/metrics_reducers.ts | 25 ++++++++- .../metrics/store/metrics_reducers_test.ts | 52 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/tensorboard/webapp/metrics/actions/index.ts b/tensorboard/webapp/metrics/actions/index.ts index f3f6d602fd..063676205a 100644 --- a/tensorboard/webapp/metrics/actions/index.ts +++ b/tensorboard/webapp/metrics/actions/index.ts @@ -277,5 +277,9 @@ export const metricsUnresolvedPinnedCardsFromLocalStorageAdded = createAction( props<{cards: CardUniqueInfo[]}>() ); +export const metricsClearAllPinnedCards = createAction( + '[Metrics] Clear all pinned cards' +); + // TODO(jieweiwu): Delete after internal code is updated. export const stepSelectorTimeSelectionChanged = timeSelectionChanged; diff --git a/tensorboard/webapp/metrics/store/metrics_reducers.ts b/tensorboard/webapp/metrics/store/metrics_reducers.ts index 2fd1165788..b6e0640e67 100644 --- a/tensorboard/webapp/metrics/store/metrics_reducers.ts +++ b/tensorboard/webapp/metrics/store/metrics_reducers.ts @@ -1525,7 +1525,30 @@ const reducer = createReducer( ], }; } - ) + ), + on(actions.metricsClearAllPinnedCards, (state) => { + let nextCardMetadataMap = {...state.cardMetadataMap}; + let nextCardStepIndexMap = {...state.cardStepIndex}; + let nextCardStateMap = {...state.cardStateMap}; + let nextLastPinnedCardTime = state.lastPinnedCardTime; + + for (const [cardId, _] of state.pinnedCardToOriginal) { + delete nextCardMetadataMap[cardId]; + delete nextCardStepIndexMap[cardId]; + delete nextCardStateMap[cardId]; + } + + return { + ...state, + cardMetadataMap: nextCardMetadataMap, + cardStateMap: nextCardStateMap, + cardStepIndex: nextCardStepIndexMap, + cardToPinnedCopy: new Map(), + cardToPinnedCopyCache: new Map(), + pinnedCardToOriginal: new Map(), + lastPinnedCardTime: nextLastPinnedCardTime, + }; + }) ); export function reducers(state: MetricsState | undefined, action: Action) { diff --git a/tensorboard/webapp/metrics/store/metrics_reducers_test.ts b/tensorboard/webapp/metrics/store/metrics_reducers_test.ts index c2658587f4..9f72c28a09 100644 --- a/tensorboard/webapp/metrics/store/metrics_reducers_test.ts +++ b/tensorboard/webapp/metrics/store/metrics_reducers_test.ts @@ -4488,5 +4488,57 @@ describe('metrics reducers', () => { expect(state2.unresolvedImportedPinnedCards).toEqual([fakePinnedCard]); }); }); + + describe('#metricsClearAllPinnedCards', () => { + it('unpins all pinned cards', () => { + const beforeState = buildMetricsState({ + cardMetadataMap: { + card1: createScalarCardMetadata(), + pinnedCopy1: createScalarCardMetadata(), + card2: createScalarCardMetadata(), + pinnedCopy2: createScalarCardMetadata(), + }, + cardList: ['card1', 'card2'], + cardStepIndex: { + card1: buildStepIndexMetadata({index: 10}), + pinnedCopy1: buildStepIndexMetadata({index: 20}), + card2: buildStepIndexMetadata({index: 11}), + pinnedCopy2: buildStepIndexMetadata({index: 21}), + }, + cardToPinnedCopy: new Map([ + ['card1', 'pinnedCopy1'], + ['card2', 'pinnedCopy2'], + ]), + cardToPinnedCopyCache: new Map([ + ['card1', 'pinnedCopy1'], + ['card2', 'pinnedCopy2'], + ]), + pinnedCardToOriginal: new Map([ + ['pinnedCopy1', 'card1'], + ['pinnedCopy2', 'card2'], + ]), + }); + const nextState = reducers( + beforeState, + actions.metricsClearAllPinnedCards() + ); + + const expectedState = buildMetricsState({ + cardMetadataMap: { + card1: createScalarCardMetadata(), + card2: createScalarCardMetadata(), + }, + cardList: ['card1', 'card2'], + cardStepIndex: { + card1: buildStepIndexMetadata({index: 10}), + card2: buildStepIndexMetadata({index: 11}), + }, + cardToPinnedCopy: new Map(), + cardToPinnedCopyCache: new Map(), + pinnedCardToOriginal: new Map(), + }); + expect(nextState).toEqual(expectedState); + }); + }); }); }); From a04c2d7714d6189be72aab00832654625899c5c0 Mon Sep 17 00:00:00 2001 From: Seayeon Lee Date: Thu, 11 Apr 2024 08:45:42 +0000 Subject: [PATCH 2/7] add clear all pins button in the pinned view container --- .../main_view/pinned_view_component.scss | 19 ++++++++ .../views/main_view/pinned_view_component.ts | 46 ++++++++++++------- .../views/main_view/pinned_view_container.ts | 6 +++ 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.scss b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.scss index f6cc2f02bb..8f8df63b88 100644 --- a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.scss +++ b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.scss @@ -26,6 +26,25 @@ mat-icon { margin-right: 5px; } +.group-toolbar { + justify-content: space-between; +} + +.left-items { + display: flex; + align-items: center; +} + +.right-items { + button { + $_height: 25px; + font-size: 12px; + font-weight: normal; + height: $_height; + line-height: $_height; + } +} + .group-text { display: flex; align-items: baseline; diff --git a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts index 5852ffb9a5..bb0f024d80 100644 --- a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts +++ b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts @@ -12,7 +12,13 @@ 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 {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; import {CardObserver} from '../card_renderer/card_lazy_loader'; import {CardIdWithMetadata} from '../metrics_view_types'; @@ -20,23 +26,30 @@ import {CardIdWithMetadata} from '../metrics_view_types'; selector: 'metrics-pinned-view-component', template: `
- - - Pinned - {{ cardIdsWithMetadata.length }} cards - - New card pinned + + + Pinned + {{ cardIdsWithMetadata.length }} cards + + New card pinned + - +
+
+ +
(); } diff --git a/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts b/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts index 67b9a60216..78e8d1f587 100644 --- a/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts @@ -21,6 +21,7 @@ import {DeepReadonly} from '../../../util/types'; import {getLastPinnedCardTime, getPinnedCardsWithMetadata} from '../../store'; import {CardObserver} from '../card_renderer/card_lazy_loader'; import {CardIdWithMetadata} from '../metrics_view_types'; +import {metricsClearAllPinnedCards} from '../../actions'; @Component({ selector: 'metrics-pinned-view', @@ -29,6 +30,7 @@ import {CardIdWithMetadata} from '../metrics_view_types'; [cardIdsWithMetadata]="cardIdsWithMetadata$ | async" [lastPinnedCardTime]="lastPinnedCardTime$ | async" [cardObserver]="cardObserver" + (onClearAllPinsClicked)="onClearAllPinsClicked()" > `, changeDetection: ChangeDetectionStrategy.OnPush, @@ -47,4 +49,8 @@ export class PinnedViewContainer { // pins after page load. skip(1) ); + + onClearAllPinsClicked() { + this.store.dispatch(metricsClearAllPinnedCards()); + } } From f0c3a54e8626a82a791859c9b6010afe7764f652 Mon Sep 17 00:00:00 2001 From: Seayeon Lee Date: Thu, 11 Apr 2024 08:48:15 +0000 Subject: [PATCH 3/7] add removeAllScalarPins method in the saved pins data source --- .../metrics/data_source/saved_pins_data_source.ts | 4 ++++ .../data_source/saved_pins_data_source_test.ts | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/tensorboard/webapp/metrics/data_source/saved_pins_data_source.ts b/tensorboard/webapp/metrics/data_source/saved_pins_data_source.ts index da02bf4b11..8a71cae4c4 100644 --- a/tensorboard/webapp/metrics/data_source/saved_pins_data_source.ts +++ b/tensorboard/webapp/metrics/data_source/saved_pins_data_source.ts @@ -45,4 +45,8 @@ export class SavedPinsDataSource { } return []; } + + removeAllScalarPins(): void { + window.localStorage.setItem(SAVED_SCALAR_PINS_KEY, JSON.stringify([])); + } } diff --git a/tensorboard/webapp/metrics/data_source/saved_pins_data_source_test.ts b/tensorboard/webapp/metrics/data_source/saved_pins_data_source_test.ts index 325acc32d2..d0d7fb4e90 100644 --- a/tensorboard/webapp/metrics/data_source/saved_pins_data_source_test.ts +++ b/tensorboard/webapp/metrics/data_source/saved_pins_data_source_test.ts @@ -114,4 +114,15 @@ describe('SavedPinsDataSource Test', () => { expect(dataSource.getSavedScalarPins()).toEqual(['tag1']); }); }); + + describe('removeAllScalarPins', () => { + it('removes all existing pins', () => { + dataSource.saveScalarPin('tag3'); + dataSource.saveScalarPin('tag4'); + + dataSource.removeAllScalarPins(); + + expect(dataSource.getSavedScalarPins().length).toEqual(0); + }); + }); }); From e97b11016d4cb0d99abb17f1745eaf0a9b956bcd Mon Sep 17 00:00:00 2001 From: Seayeon Lee Date: Thu, 11 Apr 2024 09:34:36 +0000 Subject: [PATCH 4/7] add removeAllPins effect in metrics effect --- tensorboard/webapp/metrics/effects/index.ts | 21 ++++++++++- .../metrics/effects/metrics_effects_test.ts | 37 +++++++++++++++++++ tensorboard/webapp/metrics/testing.ts | 2 + 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/tensorboard/webapp/metrics/effects/index.ts b/tensorboard/webapp/metrics/effects/index.ts index af49b9fbb5..4b8061e4e1 100644 --- a/tensorboard/webapp/metrics/effects/index.ts +++ b/tensorboard/webapp/metrics/effects/index.ts @@ -316,6 +316,21 @@ export class MetricsEffects implements OnInitEffects { }) ); + private readonly removeAllPins$ = this.actions$.pipe( + ofType(actions.metricsClearAllPinnedCards), + withLatestFrom( + this.store.select(selectors.getEnableGlobalPins), + this.store.select(selectors.getShouldPersistSettings) + ), + filter( + ([, enableGlobalPins, shouldPersistSettings]) => + enableGlobalPins && shouldPersistSettings + ), + tap(() => { + this.savedPinsDataSource.removeAllScalarPins(); + }) + ); + /** * In general, this effect dispatch the following actions: * @@ -356,7 +371,11 @@ export class MetricsEffects implements OnInitEffects { /** * Subscribes to: dashboard shown (initAction). */ - this.loadSavedPins$ + this.loadSavedPins$, + /** + * Subscribes to: metricsClearAllPinnedCards. + */ + this.removeAllPins$ ); }, {dispatch: false} diff --git a/tensorboard/webapp/metrics/effects/metrics_effects_test.ts b/tensorboard/webapp/metrics/effects/metrics_effects_test.ts index 4dfbccdb2e..bf0bb695f3 100644 --- a/tensorboard/webapp/metrics/effects/metrics_effects_test.ts +++ b/tensorboard/webapp/metrics/effects/metrics_effects_test.ts @@ -985,5 +985,42 @@ describe('metrics effects', () => { expect(actualActions).toEqual([]); }); }); + + describe('removeAllPins', () => { + let removeAllScalarPinsSpy: jasmine.Spy; + + beforeEach(() => { + removeAllScalarPinsSpy = spyOn( + savedPinsDataSource, + 'removeAllScalarPins' + ); + store.overrideSelector(selectors.getEnableGlobalPins, true); + store.refreshState(); + }); + + it('removes all pins by calling removeAllScalarPins method', () => { + actions$.next(actions.metricsClearAllPinnedCards()); + + expect(removeAllScalarPinsSpy).toHaveBeenCalled(); + }); + + it('does not remove pins if getEnableGlobalPins is false', () => { + store.overrideSelector(selectors.getEnableGlobalPins, false); + store.refreshState(); + + actions$.next(actions.metricsClearAllPinnedCards()); + + expect(removeAllScalarPinsSpy).not.toHaveBeenCalled(); + }); + + it('does not remove pins if getShouldPersistSettings is false', () => { + store.overrideSelector(selectors.getShouldPersistSettings, false); + store.refreshState(); + + actions$.next(actions.metricsClearAllPinnedCards()); + + expect(removeAllScalarPinsSpy).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/tensorboard/webapp/metrics/testing.ts b/tensorboard/webapp/metrics/testing.ts index a941997693..ea65d86d69 100644 --- a/tensorboard/webapp/metrics/testing.ts +++ b/tensorboard/webapp/metrics/testing.ts @@ -405,6 +405,8 @@ export class TestingSavedPinsDataSource { getSavedScalarPins() { return []; } + + removeAllScalarPins() {} } export function provideTestingSavedPinsDataSource() { From c0edd3767d44cff377ce20e714853fb13b51a415 Mon Sep 17 00:00:00 2001 From: Seayeon Lee Date: Thu, 11 Apr 2024 09:40:56 +0000 Subject: [PATCH 5/7] add globalPinsEnabled feature flag to the pinned view container --- .../metrics/views/main_view/main_view_test.ts | 68 ++++++++++++++++++- .../views/main_view/pinned_view_component.ts | 12 +++- .../views/main_view/pinned_view_container.ts | 4 ++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/tensorboard/webapp/metrics/views/main_view/main_view_test.ts b/tensorboard/webapp/metrics/views/main_view/main_view_test.ts index 5793a6a2e3..9ce15ee4a7 100644 --- a/tensorboard/webapp/metrics/views/main_view/main_view_test.ts +++ b/tensorboard/webapp/metrics/views/main_view/main_view_test.ts @@ -69,6 +69,7 @@ import {MainViewComponent, SHARE_BUTTON_COMPONENT} from './main_view_component'; import {MainViewContainer} from './main_view_container'; import {PinnedViewComponent} from './pinned_view_component'; import {PinnedViewContainer} from './pinned_view_container'; +import {buildMockState} from '../../../testing/utils'; @Component({ selector: 'card-view', @@ -182,7 +183,11 @@ describe('metrics main view', () => { ], providers: [ provideMockStore({ - initialState: appStateFromMetricsState(buildMetricsState()), + initialState: { + ...buildMockState({ + ...appStateFromMetricsState(buildMetricsState()), + }), + }, }), ], // Skip errors for card renderers, which are tested separately. @@ -1606,6 +1611,67 @@ describe('metrics main view', () => { expect(indicator).toBeTruthy(); }); }); + + describe('clear all pins button', () => { + beforeEach(() => { + store.overrideSelector(selectors.getEnableGlobalPins, true); + }); + + it('does not show the button if getEnableGlobalPins is false', () => { + store.overrideSelector(selectors.getEnableGlobalPins, false); + store.overrideSelector(selectors.getPinnedCardsWithMetadata, []); + const fixture = TestBed.createComponent(MainViewContainer); + fixture.detectChanges(); + + const clearAllButton = fixture.debugElement.query( + By.css('[aria-label="Clear all pinned cards"]') + ); + expect(clearAllButton).toBeNull(); + }); + + it('does not show the button if there is no pinned card', () => { + store.overrideSelector(selectors.getPinnedCardsWithMetadata, []); + const fixture = TestBed.createComponent(MainViewContainer); + fixture.detectChanges(); + + const clearAllButton = fixture.debugElement.query( + By.css('[aria-label="Clear all pinned cards"]') + ); + expect(clearAllButton).toBeNull(); + }); + + it('shows the button if there is a pinned card', () => { + store.overrideSelector(selectors.getPinnedCardsWithMetadata, [ + {cardId: 'card1', ...createCardMetadata(PluginType.SCALARS)}, + {cardId: 'card2', ...createCardMetadata(PluginType.IMAGES)}, + ]); + const fixture = TestBed.createComponent(MainViewContainer); + fixture.detectChanges(); + + const clearAllButton = fixture.debugElement.query( + By.css('[aria-label="Clear all pinned cards"]') + ); + expect(clearAllButton).toBeTruthy(); + }); + + it('dispatch clear all action when the button is clicked', () => { + store.overrideSelector(selectors.getPinnedCardsWithMetadata, [ + {cardId: 'card1', ...createCardMetadata(PluginType.SCALARS)}, + {cardId: 'card2', ...createCardMetadata(PluginType.IMAGES)}, + ]); + const fixture = TestBed.createComponent(MainViewContainer); + fixture.detectChanges(); + + const clearAllButton = fixture.debugElement.query( + By.css('[aria-label="Clear all pinned cards"]') + ); + clearAllButton.nativeElement.click(); + + expect(dispatchedActions).toEqual([ + actions.metricsClearAllPinnedCards(), + ]); + }); + }); }); describe('slideout menu', () => { diff --git a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts index bb0f024d80..3d8583bf34 100644 --- a/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts +++ b/tensorboard/webapp/metrics/views/main_view/pinned_view_component.ts @@ -45,8 +45,15 @@ import {CardIdWithMetadata} from '../metrics_view_types'; -
-
@@ -67,5 +74,6 @@ export class PinnedViewComponent { @Input() cardObserver!: CardObserver; @Input() cardIdsWithMetadata!: CardIdWithMetadata[]; @Input() lastPinnedCardTime!: number; + @Input() globalPinsEnabled: boolean = false; @Output() onClearAllPinsClicked = new EventEmitter(); } diff --git a/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts b/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts index 78e8d1f587..c280008c93 100644 --- a/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts +++ b/tensorboard/webapp/metrics/views/main_view/pinned_view_container.ts @@ -22,6 +22,7 @@ import {getLastPinnedCardTime, getPinnedCardsWithMetadata} from '../../store'; import {CardObserver} from '../card_renderer/card_lazy_loader'; import {CardIdWithMetadata} from '../metrics_view_types'; import {metricsClearAllPinnedCards} from '../../actions'; +import {getEnableGlobalPins} from '../../../selectors'; @Component({ selector: 'metrics-pinned-view', @@ -30,6 +31,7 @@ import {metricsClearAllPinnedCards} from '../../actions'; [cardIdsWithMetadata]="cardIdsWithMetadata$ | async" [lastPinnedCardTime]="lastPinnedCardTime$ | async" [cardObserver]="cardObserver" + [globalPinsEnabled]="globalPinsEnabled$ | async" (onClearAllPinsClicked)="onClearAllPinsClicked()" > `, @@ -50,6 +52,8 @@ export class PinnedViewContainer { skip(1) ); + readonly globalPinsEnabled$ = this.store.select(getEnableGlobalPins); + onClearAllPinsClicked() { this.store.dispatch(metricsClearAllPinnedCards()); } From d16ba83f32144423189f3bfbbaf60f3a9b9f9d3e Mon Sep 17 00:00:00 2001 From: Seayeon Lee Date: Thu, 11 Apr 2024 14:25:12 +0000 Subject: [PATCH 6/7] fix typescript issues --- tensorboard/webapp/metrics/store/metrics_reducers.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tensorboard/webapp/metrics/store/metrics_reducers.ts b/tensorboard/webapp/metrics/store/metrics_reducers.ts index b6e0640e67..aa8f0fc197 100644 --- a/tensorboard/webapp/metrics/store/metrics_reducers.ts +++ b/tensorboard/webapp/metrics/store/metrics_reducers.ts @@ -77,6 +77,8 @@ import { TagMetadata, TimeSeriesData, TimeSeriesLoadable, + CardToPinnedCard, + PinnedCardToCard, } from './metrics_types'; import {dataTableUtils} from '../../widgets/data_table/utils'; @@ -1532,7 +1534,7 @@ const reducer = createReducer( let nextCardStateMap = {...state.cardStateMap}; let nextLastPinnedCardTime = state.lastPinnedCardTime; - for (const [cardId, _] of state.pinnedCardToOriginal) { + for (const cardId of state.pinnedCardToOriginal.keys()) { delete nextCardMetadataMap[cardId]; delete nextCardStepIndexMap[cardId]; delete nextCardStateMap[cardId]; @@ -1543,9 +1545,9 @@ const reducer = createReducer( cardMetadataMap: nextCardMetadataMap, cardStateMap: nextCardStateMap, cardStepIndex: nextCardStepIndexMap, - cardToPinnedCopy: new Map(), - cardToPinnedCopyCache: new Map(), - pinnedCardToOriginal: new Map(), + cardToPinnedCopy: new Map() as CardToPinnedCard, + cardToPinnedCopyCache: new Map() as CardToPinnedCard, + pinnedCardToOriginal: new Map() as PinnedCardToCard, lastPinnedCardTime: nextLastPinnedCardTime, }; }) From 84d4f991a227a8e10b022b44902552e05209dbe1 Mon Sep 17 00:00:00 2001 From: Seayeon Lee Date: Sat, 13 Apr 2024 09:33:42 +0000 Subject: [PATCH 7/7] remove unused variables and unchanged fields to const --- tensorboard/webapp/metrics/store/metrics_reducers.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tensorboard/webapp/metrics/store/metrics_reducers.ts b/tensorboard/webapp/metrics/store/metrics_reducers.ts index aa8f0fc197..8a6abda21b 100644 --- a/tensorboard/webapp/metrics/store/metrics_reducers.ts +++ b/tensorboard/webapp/metrics/store/metrics_reducers.ts @@ -1529,14 +1529,13 @@ const reducer = createReducer( } ), on(actions.metricsClearAllPinnedCards, (state) => { - let nextCardMetadataMap = {...state.cardMetadataMap}; - let nextCardStepIndexMap = {...state.cardStepIndex}; - let nextCardStateMap = {...state.cardStateMap}; - let nextLastPinnedCardTime = state.lastPinnedCardTime; + const nextCardMetadataMap = {...state.cardMetadataMap}; + const nextCardStepIndex = {...state.cardStepIndex}; + const nextCardStateMap = {...state.cardStateMap}; for (const cardId of state.pinnedCardToOriginal.keys()) { delete nextCardMetadataMap[cardId]; - delete nextCardStepIndexMap[cardId]; + delete nextCardStepIndex[cardId]; delete nextCardStateMap[cardId]; } @@ -1544,11 +1543,10 @@ const reducer = createReducer( ...state, cardMetadataMap: nextCardMetadataMap, cardStateMap: nextCardStateMap, - cardStepIndex: nextCardStepIndexMap, + cardStepIndex: nextCardStepIndex, cardToPinnedCopy: new Map() as CardToPinnedCard, cardToPinnedCopyCache: new Map() as CardToPinnedCard, pinnedCardToOriginal: new Map() as PinnedCardToCard, - lastPinnedCardTime: nextLastPinnedCardTime, }; }) );