diff --git a/changelogs/fragments/9072.yml b/changelogs/fragments/9072.yml new file mode 100644 index 000000000000..00c7f2f22d63 --- /dev/null +++ b/changelogs/fragments/9072.yml @@ -0,0 +1,2 @@ +fix: +- Preserve location state at dashboard app startup to fix adding a new visualization ([#9072](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9072)) \ No newline at end of file diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 3fc1dd35f813..6a7c203a7b0a 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -918,6 +918,26 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "getPerPage": [Function], }, }, + "scopedHistory": Object { + "action": "PUSH", + "block": [MockFunction], + "createHref": [MockFunction], + "createSubHistory": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "length": 1, + "listen": [MockFunction], + "location": Object { + "hash": "", + "key": undefined, + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [MockFunction], + "replace": [MockFunction], + }, "toastNotifications": Object { "add": [MockFunction], "addDanger": [MockFunction], @@ -2006,6 +2026,26 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "getPerPage": [Function], }, }, + "scopedHistory": Object { + "action": "PUSH", + "block": [MockFunction], + "createHref": [MockFunction], + "createSubHistory": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "length": 1, + "listen": [MockFunction], + "location": Object { + "hash": "", + "key": undefined, + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [MockFunction], + "replace": [MockFunction], + }, "toastNotifications": Object { "add": [MockFunction], "addDanger": [MockFunction], @@ -3094,6 +3134,26 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "getPerPage": [Function], }, }, + "scopedHistory": Object { + "action": "PUSH", + "block": [MockFunction], + "createHref": [MockFunction], + "createSubHistory": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "length": 1, + "listen": [MockFunction], + "location": Object { + "hash": "", + "key": undefined, + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [MockFunction], + "replace": [MockFunction], + }, "toastNotifications": Object { "add": [MockFunction], "addDanger": [MockFunction], @@ -4182,6 +4242,26 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "getPerPage": [Function], }, }, + "scopedHistory": Object { + "action": "PUSH", + "block": [MockFunction], + "createHref": [MockFunction], + "createSubHistory": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "length": 1, + "listen": [MockFunction], + "location": Object { + "hash": "", + "key": undefined, + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [MockFunction], + "replace": [MockFunction], + }, "toastNotifications": Object { "add": [MockFunction], "addDanger": [MockFunction], @@ -5270,6 +5350,26 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "getPerPage": [Function], }, }, + "scopedHistory": Object { + "action": "PUSH", + "block": [MockFunction], + "createHref": [MockFunction], + "createSubHistory": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "length": 1, + "listen": [MockFunction], + "location": Object { + "hash": "", + "key": undefined, + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [MockFunction], + "replace": [MockFunction], + }, "toastNotifications": Object { "add": [MockFunction], "addDanger": [MockFunction], @@ -6358,6 +6458,26 @@ exports[`Dashboard top nav render with all components 1`] = ` "getPerPage": [Function], }, }, + "scopedHistory": Object { + "action": "PUSH", + "block": [MockFunction], + "createHref": [MockFunction], + "createSubHistory": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "length": 1, + "listen": [MockFunction], + "location": Object { + "hash": "", + "key": undefined, + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [MockFunction], + "replace": [MockFunction], + }, "toastNotifications": Object { "add": [MockFunction], "addDanger": [MockFunction], diff --git a/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.test.tsx index 1c57e1087be0..bd5b70f986be 100644 --- a/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.test.tsx @@ -17,6 +17,7 @@ import { createDashboardServicesMock } from './mocks'; import { SavedObjectDashboard } from '../..'; import { syncQueryStateWithUrl } from 'src/plugins/data/public'; import { ViewMode } from 'src/plugins/embeddable/public'; +import { scopedHistoryMock } from '../../../../../core/public/mocks'; const mockStartStateSync = jest.fn(); const mockStopStateSync = jest.fn(); @@ -48,7 +49,7 @@ const { createStateContainer, syncState } = jest.requireMock( const osdUrlStateStorage = ({ set: jest.fn(), get: jest.fn(() => ({ linked: false })), - flush: jest.fn(), + flush: jest.fn().mockReturnValue(true), } as unknown) as IOsdUrlStateStorage; describe('createDashboardGlobalAndAppState', () => { @@ -148,13 +149,47 @@ describe('updateStateUrl', () => { ...dashboardAppStateStub, viewMode: ViewMode.VIEW, }; - updateStateUrl({ osdUrlStateStorage, state: dashboardAppState, replace: true }); test('update URL to not contain panels', () => { const { panels, ...statesWithoutPanels } = dashboardAppState; + + const basePath = '/base'; + const history = scopedHistoryMock.create({ + pathname: basePath, + }); + + updateStateUrl({ + osdUrlStateStorage, + state: dashboardAppState, + scopedHistory: history, + replace: true, + }); + expect(osdUrlStateStorage.set).toHaveBeenCalledWith('_a', statesWithoutPanels, { replace: true, }); expect(osdUrlStateStorage.flush).toHaveBeenCalledWith({ replace: true }); }); + + test('preserve Dashboards scoped history state', () => { + const basePath = '/base'; + const someState = { some: 'state' }; + const history = scopedHistoryMock.create({ + pathname: basePath, + state: someState, + }); + const { location } = history; + const replaceSpy = jest.spyOn(history, 'replace'); + + const changed = updateStateUrl({ + osdUrlStateStorage, + state: dashboardAppState, + scopedHistory: history, + replace: true, + }); + + expect(history.location.state).toEqual(someState); + expect(changed).toBe(true); + expect(replaceSpy).toHaveBeenCalledWith({ ...location, state: someState }); + }); }); diff --git a/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx b/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx index bd6d55b8bc03..a0303ca1b2b1 100644 --- a/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx +++ b/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ScopedHistory } from 'src/core/public'; import { migrateAppState } from '../utils/migrate_app_state'; import { IOsdUrlStateStorage, @@ -40,13 +41,14 @@ export const createDashboardGlobalAndAppState = ({ opensearchDashboardsVersion, usageCollection, history, + scopedHistory, data: { query }, } = services; - /* + /* Function migrateAppState() does two things 1. Migrate panel before version 7.3.0 to the 7.3.0 panel structure. - There are no changes to the panel structure after version 7.3.0 to the current + There are no changes to the panel structure after version 7.3.0 to the current OpenSearch version so no need to migrate panels that are version 7.3.0 or higher 2. Update the version number on each panel to the current version. */ @@ -131,7 +133,7 @@ export const createDashboardGlobalAndAppState = ({ osdUrlStateStorage ); - updateStateUrl({ osdUrlStateStorage, state: initialState, replace: true }); + updateStateUrl({ osdUrlStateStorage, state: initialState, scopedHistory, replace: true }); // start syncing the appState with the ('_a') url startStateSync(); return { stateContainer, stopStateSync, stopSyncingQueryServiceStateWithUrl }; @@ -147,15 +149,26 @@ export const createDashboardGlobalAndAppState = ({ export const updateStateUrl = ({ osdUrlStateStorage, state, + scopedHistory, replace, }: { osdUrlStateStorage: IOsdUrlStateStorage; state: DashboardAppState; + scopedHistory: ScopedHistory; replace: boolean; }) => { osdUrlStateStorage.set(APP_STATE_STORAGE_KEY, toUrlState(state), { replace }); // immediately forces scheduled updates and changes location - return osdUrlStateStorage.flush({ replace }); + // scoped history state is preserved to allow embeddable state transfer + const previousState = scopedHistory.location.state; + const changed = osdUrlStateStorage.flush({ replace }); + if (changed) { + scopedHistory.replace({ + ...scopedHistory.location, + state: previousState, + }); + } + return changed; }; const toUrlState = (state: DashboardAppState): DashboardAppStateInUrl => { diff --git a/src/plugins/dashboard/public/application/utils/mocks.ts b/src/plugins/dashboard/public/application/utils/mocks.ts index 84720b0bcbc4..529808eb6258 100644 --- a/src/plugins/dashboard/public/application/utils/mocks.ts +++ b/src/plugins/dashboard/public/application/utils/mocks.ts @@ -9,6 +9,8 @@ import { dashboardPluginMock } from '../../../../dashboard/public/mocks'; import { usageCollectionPluginMock } from '../../../../usage_collection/public/mocks'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; import { DashboardServices } from '../../types'; +import { scopedHistoryMock } from '../../../../../core/public/mocks'; +import { ScopedHistory } from '../../../../../core/public'; export const createDashboardServicesMock = () => { const coreStartMock = coreMock.createStart(); @@ -18,6 +20,7 @@ export const createDashboardServicesMock = () => { const usageCollection = usageCollectionPluginMock.createSetupContract(); const embeddable = embeddablePluginMock.createStartContract(); const opensearchDashboardsVersion = '3.0.0'; + const scopedHistory = (scopedHistoryMock.create() as unknown) as ScopedHistory; return ({ ...coreStartMock, @@ -27,6 +30,7 @@ export const createDashboardServicesMock = () => { replace: jest.fn(), location: { pathname: '' }, }, + scopedHistory, dashboardConfig: { getHideWriteControls: jest.fn(), }, diff --git a/src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx b/src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx index 9efb4c4b2a13..a31e1eb45fdc 100644 --- a/src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx +++ b/src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx @@ -54,6 +54,7 @@ export const useDashboardAppAndGlobalState = ({ usageCollection, opensearchDashboardsVersion, osdUrlStateStorage, + scopedHistory, } = services; const hideWriteControls = dashboardConfig.getHideWriteControls(); const stateDefaults = migrateAppState( @@ -136,6 +137,7 @@ export const useDashboardAppAndGlobalState = ({ const updated = updateStateUrl({ osdUrlStateStorage, state: stateContainer.getState(), + scopedHistory, replace, });