diff --git a/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts b/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts index 6d90316098fb5..cbfc7271aef41 100644 --- a/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts +++ b/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts @@ -37,28 +37,35 @@ export const isPanelVersionTooOld = (panels: SavedDashboardPanel[]) => { return false; }; +function getPanelsMap(appStateInUrl: SharedDashboardState): DashboardPanelMap | undefined { + if (!appStateInUrl.panels) { + return undefined; + } + + if (appStateInUrl.panels.length === 0) { + return {}; + } + + if (isPanelVersionTooOld(appStateInUrl.panels)) { + pluginServices.getServices().notifications.toasts.addWarning(getPanelTooOldErrorString()); + return undefined; + } + + return convertSavedPanelsToPanelMap(appStateInUrl.panels); +} + /** * Loads any dashboard state from the URL, and removes the state from the URL. */ export const loadAndRemoveDashboardState = ( kbnUrlStateStorage: IKbnUrlStateStorage ): Partial => { - const { - notifications: { toasts }, - } = pluginServices.getServices(); const rawAppStateInUrl = kbnUrlStateStorage.get( DASHBOARD_STATE_STORAGE_KEY ); if (!rawAppStateInUrl) return {}; - let panelsMap: DashboardPanelMap | undefined; - if (rawAppStateInUrl.panels && rawAppStateInUrl.panels.length > 0) { - if (isPanelVersionTooOld(rawAppStateInUrl.panels)) { - toasts.addWarning(getPanelTooOldErrorString()); - } else { - panelsMap = convertSavedPanelsToPanelMap(rawAppStateInUrl.panels); - } - } + const panelsMap = getPanelsMap(rawAppStateInUrl); const nextUrl = replaceUrlHashQuery(window.location.href, (hashQuery) => { delete hashQuery[DASHBOARD_STATE_STORAGE_KEY]; diff --git a/test/functional/apps/dashboard/group3/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts index c280de155be82..7839315cb2239 100644 --- a/test/functional/apps/dashboard/group3/dashboard_state.ts +++ b/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -8,13 +8,15 @@ import expect from '@kbn/expect'; import chroma from 'chroma-js'; - +import rison from '@kbn/rison'; import { DEFAULT_PANEL_WIDTH } from '@kbn/dashboard-plugin/public/dashboard_constants'; +import type { SharedDashboardState } from '@kbn/dashboard-plugin/common'; import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ + 'common', 'dashboard', 'visualize', 'header', @@ -31,6 +33,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const dashboardAddPanel = getService('dashboardAddPanel'); const xyChartSelector = 'xyVisChart'; + const log = getService('log'); + + const updateAppStateQueryParam = ( + url: string, + setAppState: (appState: Partial) => Partial + ) => { + log.debug(`updateAppStateQueryParam, before url: ${url}`); + + // Using lastIndexOf because URL may have 2 sets of query parameters. + // 1) server query parameters, '_t' + // 2) client query parameters, '_g' and '_a'. Anything after the '#' in a URL is used by the client + // Example shape of URL http://localhost:5620/app/dashboards?_t=12345#/create?_g=() + const clientQueryParamsStartIndex = url.lastIndexOf('?'); + if (clientQueryParamsStartIndex === -1) { + throw Error(`Unable to locate query parameters in URL: ${url}`); + } + const urlBeforeClientQueryParams = url.substring(0, clientQueryParamsStartIndex); + const urlParams = new URLSearchParams(url.substring(clientQueryParamsStartIndex + 1)); + const appState: Partial = urlParams.has('_a') + ? (rison.decode(urlParams.get('_a')!) as Partial) + : {}; + const newAppState = { + ...appState, + ...setAppState(appState), + }; + urlParams.set('_a', rison.encode(newAppState)); + const newUrl = urlBeforeClientQueryParams + '?' + urlParams.toString(); + log.debug(`updateAppStateQueryParam, after url: ${newUrl}`); + return newUrl; + }; const enableNewChartLibraryDebug = async (force = false) => { if ((await PageObjects.visChart.isNewChartsLibraryEnabled()) || force) { @@ -39,10 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } }; - // Failing: See https://github.com/elastic/kibana/issues/139762 - describe.skip('dashboard state', function describeIndexTests() { - // Used to track flag before and after reset - + describe('dashboard state', function () { before(async function () { await PageObjects.dashboard.initTests(); await PageObjects.dashboard.preserveCrossAppState(); @@ -140,8 +169,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Saved search will update when the query is changed in the URL', async () => { const currentQuery = await queryBar.getQueryString(); expect(currentQuery).to.equal(''); - const currentUrl = await getUrlFromShare(); - const newUrl = currentUrl.replace(`query:''`, `query:'abc12345678910'`); + const newUrl = updateAppStateQueryParam( + await getUrlFromShare(), + (appState: Partial) => { + return { + query: { + language: 'kuery', + query: 'abc12345678910', + }, + }; + } + ); // We need to add a timestamp to the URL because URL changes now only work with a hard refresh. await browser.get(newUrl.toString()); @@ -153,9 +191,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); const getUrlFromShare = async () => { + log.debug(`getUrlFromShare`); await PageObjects.share.clickShareTopNavButton(); const sharedUrl = await PageObjects.share.getSharedUrl(); await PageObjects.share.clickShareTopNavButton(); + log.debug(`sharedUrl: ${sharedUrl}`); return sharedUrl; }; @@ -177,11 +217,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const changeQuery = async (useHardRefresh: boolean, newQuery: string) => { await queryBar.clickQuerySubmitButton(); - const oldQuery = await queryBar.getQueryString(); const currentUrl = await getUrlFromShare(); - const newUrl = currentUrl.replace(`query:'${oldQuery}'`, `query:'${newQuery}'`); + const newUrl = updateAppStateQueryParam( + currentUrl, + (appState: Partial) => { + return { + query: { + language: 'kuery', + query: newQuery, + }, + }; + } + ); await browser.get(newUrl.toString(), !useHardRefresh); + await PageObjects.dashboard.waitForRenderComplete(); const queryBarContentsAfterRefresh = await queryBar.getQueryString(); expect(queryBarContentsAfterRefresh).to.equal(newQuery); }; @@ -202,9 +252,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME); const currentUrl = await getUrlFromShare(); const currentPanelDimensions = await PageObjects.dashboard.getPanelDimensions(); - const newUrl = currentUrl.replace( - `w:${DEFAULT_PANEL_WIDTH}`, - `w:${DEFAULT_PANEL_WIDTH * 2}` + const newUrl = updateAppStateQueryParam( + currentUrl, + (appState: Partial) => { + log.debug(JSON.stringify(appState, null, ' ')); + return { + panels: (appState.panels ?? []).map((panel) => { + return { + ...panel, + gridData: { + ...panel.gridData, + w: + panel.gridData.w === DEFAULT_PANEL_WIDTH + ? DEFAULT_PANEL_WIDTH * 2 + : panel.gridData.w, + }, + }; + }), + }; + } ); await hardRefresh(newUrl); @@ -229,7 +295,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('when removing a panel', async function () { await PageObjects.dashboard.waitForRenderComplete(); const currentUrl = await getUrlFromShare(); - const newUrl = currentUrl.replace(/panels:\!\(.*\),query/, 'panels:!(),query'); + const newUrl = updateAppStateQueryParam( + currentUrl, + (appState: Partial) => { + return { + panels: [], + }; + } + ); await hardRefresh(newUrl); await retry.try(async () => { @@ -255,7 +328,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await PageObjects.visChart.selectNewLegendColorChoice('#F9D9F9'); const currentUrl = await getUrlFromShare(); - const newUrl = currentUrl.replace('F9D9F9', 'FFFFFF'); + const newUrl = updateAppStateQueryParam( + currentUrl, + (appState: Partial) => { + return { + panels: (appState.panels ?? []).map((panel) => { + return { + ...panel, + embeddableConfig: { + ...(panel.embeddableConfig ?? {}), + vis: { + ...((panel.embeddableConfig?.vis as object) ?? {}), + colors: { + ...((panel.embeddableConfig?.vis as { colors: object })?.colors ?? {}), + ['80000']: 'FFFFFF', + }, + }, + }, + }; + }), + }; + } + ); await hardRefresh(newUrl); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -280,7 +374,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('resets a pie slice color to the original when removed', async function () { const currentUrl = await getUrlFromShare(); - const newUrl = currentUrl.replace(`'80000':%23FFFFFF`, ''); + const newUrl = updateAppStateQueryParam( + currentUrl, + (appState: Partial) => { + return { + panels: (appState.panels ?? []).map((panel) => { + return { + ...panel, + embeddableConfig: { + ...(panel.embeddableConfig ?? {}), + vis: { + ...((panel.embeddableConfig?.vis as object) ?? {}), + colors: {}, + }, + }, + }; + }), + }; + } + ); await hardRefresh(newUrl); await PageObjects.header.waitUntilLoadingHasFinished();