From 9e48a5775529939b8a346ea6c40eb1c7c3b0193e Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 16 Jun 2023 06:22:23 -0400 Subject: [PATCH] [8.8] [Dashboard] Fast Navigation Between Dashboards (#157437) (#159827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.8`: - [[Dashboard] Fast Navigation Between Dashboards (#157437)](https://github.com/elastic/kibana/pull/157437) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk KopyciƄski --- .../dashboard_with_controls_example.tsx | 12 +- .../public/dynamically_add_panels_example.tsx | 4 +- .../public/static_by_reference_example.tsx | 7 +- .../public/static_by_value_example.tsx | 4 +- .../embeddable/control_group_container.tsx | 13 ++ .../public/dashboard_app/dashboard_app.tsx | 48 +++-- .../public/dashboard_app/dashboard_router.tsx | 26 +-- .../top_nav/dashboard_top_nav.tsx | 3 +- .../component/grid/dashboard_grid.tsx | 22 +-- .../embeddable/api/run_save_functions.tsx | 4 - .../dashboard_control_group_integration.ts | 10 +- .../create/create_dashboard.test.ts | 52 +++-- .../embeddable/create/create_dashboard.ts | 187 +++++++++++------- ...rt_dashboard_search_session_integration.ts | 2 +- .../sync_dashboard_unified_search_state.ts | 6 +- .../embeddable/dashboard_container.tsx | 76 ++++--- .../dashboard_container_factory.tsx | 6 +- .../external_api/dashboard_renderer.test.tsx | 19 +- .../external_api/dashboard_renderer.tsx | 10 +- .../state/dashboard_container_reducers.ts | 11 ++ .../diffing/dashboard_diffing_integration.ts | 2 +- .../public/dashboard_container/types.ts | 1 + .../services/dashboard/add_panel.ts | 2 +- .../dashboards/dashboard_renderer.tsx | 2 +- 24 files changed, 299 insertions(+), 230 deletions(-) diff --git a/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx b/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx index b0ad160b0829..ce67d8525d47 100644 --- a/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx +++ b/examples/portable_dashboards_example/public/dashboard_with_controls_example.tsx @@ -14,7 +14,11 @@ import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { controlGroupInputBuilder } from '@kbn/controls-plugin/public'; import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common'; import { FILTER_DEBUGGER_EMBEDDABLE } from '@kbn/embeddable-examples-plugin/public'; -import { AwaitingDashboardAPI, DashboardRenderer } from '@kbn/dashboard-plugin/public'; +import { + AwaitingDashboardAPI, + DashboardRenderer, + DashboardCreationOptions, +} from '@kbn/dashboard-plugin/public'; export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => { const [dashboard, setDashboard] = useState(); @@ -48,7 +52,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView { + getCreationOptions={async (): Promise => { const builder = controlGroupInputBuilder; const controlGroupInput = getDefaultControlGroupInput(); await builder.addDataControlFromField(controlGroupInput, { @@ -68,11 +72,11 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView return { useControlGroupIntegration: true, - initialInput: { + getInitialInput: () => ({ timeRange: { from: 'now-30d', to: 'now' }, viewMode: ViewMode.VIEW, controlGroupInput, - }, + }), }; }} ref={setDashboard} diff --git a/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx b/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx index c1c4f7b850f2..c949727f58d6 100644 --- a/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx +++ b/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx @@ -136,10 +136,10 @@ export const DynamicByReferenceExample = () => { getCreationOptions={async () => { const persistedInput = getPersistableInput(); return { - initialInput: { + getInitialInput: () => ({ ...persistedInput, timeRange: { from: 'now-30d', to: 'now' }, // need to set the time range for the by value vis - }, + }), }; }} ref={setdashboard} diff --git a/examples/portable_dashboards_example/public/static_by_reference_example.tsx b/examples/portable_dashboards_example/public/static_by_reference_example.tsx index 3cd5bcb073a7..8d66e3ccf6ed 100644 --- a/examples/portable_dashboards_example/public/static_by_reference_example.tsx +++ b/examples/portable_dashboards_example/public/static_by_reference_example.tsx @@ -50,12 +50,15 @@ export const StaticByReferenceExample = ({ const field = dataView.getFieldByName('machine.os.keyword'); let filter: Filter; let creationOptions: DashboardCreationOptions = { - initialInput: { viewMode: ViewMode.VIEW }, + getInitialInput: () => ({ viewMode: ViewMode.VIEW }), }; if (field) { filter = buildPhraseFilter(field, 'win xp', dataView); filter.meta.negate = true; - creationOptions = { ...creationOptions, initialInput: { filters: [filter] } }; + creationOptions = { + ...creationOptions, + getInitialInput: () => ({ filters: [filter] }), + }; } return creationOptions; // if can't find the field, then just return no special creation options }} diff --git a/examples/portable_dashboards_example/public/static_by_value_example.tsx b/examples/portable_dashboards_example/public/static_by_value_example.tsx index 3fbf416fbfbf..733c340a463e 100644 --- a/examples/portable_dashboards_example/public/static_by_value_example.tsx +++ b/examples/portable_dashboards_example/public/static_by_value_example.tsx @@ -29,11 +29,11 @@ export const StaticByValueExample = () => { { return { - initialInput: { + getInitialInput: () => ({ timeRange: { from: 'now-30d', to: 'now' }, viewMode: ViewMode.VIEW, panels: panelsJson as DashboardPanelMap, - }, + }), }; }} /> diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx index 44774ba00ba8..6344c768eae6 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx +++ b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx @@ -190,6 +190,19 @@ export class ControlGroupContainer extends Container< ); }; + public updateInputAndReinitialize = (newInput: Partial) => { + this.subscriptions.unsubscribe(); + this.subscriptions = new Subscription(); + this.initialized$.next(false); + this.updateInput(newInput); + this.untilAllChildrenReady().then(() => { + this.recalculateDataViews(); + this.recalculateFilters(); + this.setupSubscriptions(); + this.initialized$.next(true); + }); + }; + public setLastUsedDataViewId = (lastUsedDataViewId: string) => { this.lastUsedDataViewId = lastUsedDataViewId; }; diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx index 8fc546a41ea6..cac18a72d2e1 100644 --- a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx +++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx @@ -83,11 +83,6 @@ export function DashboardApp({ customBranding, } = pluginServices.getServices(); const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false); - - const incomingEmbeddable = getStateTransfer().getIncomingEmbeddablePackage( - DASHBOARD_APP_ID, - true - ); const { scopedHistory: getScopedHistory } = useDashboardMountContext(); useExecutionContext(executionContext, { @@ -125,13 +120,28 @@ export function DashboardApp({ /** * Create options to pass into the dashboard renderer */ - const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory); const getCreationOptions = useCallback((): Promise => { - const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage); const searchSessionIdFromURL = getSearchSessionIdFromURL(history); + const getInitialInput = () => { + const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory); + const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage); + + // Override all state with URL + Locator input + return { + // State loaded from the dashboard app URL and from the locator overrides all other dashboard state. + ...initialUrlState, + ...stateFromLocator, + + // if print mode is active, force viewMode.PRINT + ...(isScreenshotMode() && getScreenshotContext('layout') === 'print' + ? { viewMode: ViewMode.PRINT } + : {}), + }; + }; - return Promise.resolve({ - incomingEmbeddable, + return Promise.resolve({ + getIncomingEmbeddable: () => + getStateTransfer().getIncomingEmbeddablePackage(DASHBOARD_APP_ID, true), // integrations useControlGroupIntegration: true, @@ -148,28 +158,16 @@ export function DashboardApp({ getSearchSessionIdFromURL: () => getSearchSessionIdFromURL(history), removeSessionIdFromUrl: () => removeSearchSessionIdFromURL(kbnUrlStateStorage), }, - - // Override all state with URL + Locator input - initialInput: { - // State loaded from the dashboard app URL and from the locator overrides all other dashboard state. - ...initialUrlState, - ...stateFromLocator, - - // if print mode is active, force viewMode.PRINT - ...(isScreenshotMode() && getScreenshotContext('layout') === 'print' - ? { viewMode: ViewMode.PRINT } - : {}), - }, - + getInitialInput, validateLoadedSavedObject: validateOutcome, }); }, [ history, validateOutcome, - stateFromLocator, + getScopedHistory, isScreenshotMode, + getStateTransfer, kbnUrlStateStorage, - incomingEmbeddable, getScreenshotContext, ]); @@ -183,7 +181,7 @@ export function DashboardApp({ dashboardAPI, }); return () => stopWatchingAppStateInUrl(); - }, [dashboardAPI, kbnUrlStateStorage]); + }, [dashboardAPI, kbnUrlStateStorage, savedDashboardId]); return (
diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx index 6956ea7024a3..e2b4c22a40b4 100644 --- a/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx +++ b/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx @@ -9,13 +9,13 @@ import './_dashboard_app.scss'; import React from 'react'; -import { History } from 'history'; import { parse, ParsedQuery } from 'query-string'; import { render, unmountComponentAtNode } from 'react-dom'; import { Switch, RouteComponentProps, HashRouter, Redirect } from 'react-router-dom'; import { Route } from '@kbn/shared-ux-router'; import { I18nProvider } from '@kbn/i18n-react'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; import { AppMountParameters, CoreSetup } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; @@ -56,6 +56,7 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da chrome: { setBadge, docTitle, setHelpExtension }, dashboardCapabilities: { showWriteControls }, documentationLinks: { dashboardDocLink }, + application: { navigateToApp }, settings: { uiSettings }, data: dataStart, notifications, @@ -63,7 +64,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da } = pluginServices.getServices(); let globalEmbedSettings: DashboardEmbedSettings | undefined; - let routerHistory: History; const getUrlStateStorage = (history: RouteComponentProps['history']) => createKbnUrlStateStorage({ @@ -73,17 +73,17 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da }); const redirect = (redirectTo: RedirectToProps) => { - if (!routerHistory) return; - const historyFunction = redirectTo.useReplace ? routerHistory.replace : routerHistory.push; - let destination; + let path; + let state; if (redirectTo.destination === 'dashboard') { - destination = redirectTo.id - ? createDashboardEditUrl(redirectTo.id, redirectTo.editMode) - : CREATE_NEW_DASHBOARD_URL; + path = redirectTo.id ? createDashboardEditUrl(redirectTo.id) : CREATE_NEW_DASHBOARD_URL; + if (redirectTo.editMode) { + state = { viewMode: ViewMode.EDIT }; + } } else { - destination = createDashboardListingFilterUrl(redirectTo.filter); + path = createDashboardListingFilterUrl(redirectTo.filter); } - historyFunction(destination); + navigateToApp(DASHBOARD_APP_ID, { path: `#/${path}`, state, replace: redirectTo.useReplace }); }; const getDashboardEmbedSettings = ( @@ -102,9 +102,6 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da if (routeParams.embed && !globalEmbedSettings) { globalEmbedSettings = getDashboardEmbedSettings(routeParams); } - if (!routerHistory) { - routerHistory = routeProps.history; - } return ( state.explicitInput.title); // store data views in state & subscribe to dashboard data view changes. - const [allDataViews, setAllDataViews] = useState(dashboard.getAllDataViews()); + const [allDataViews, setAllDataViews] = useState([]); useEffect(() => { + setAllDataViews(dashboard.getAllDataViews()); const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) => setAllDataViews(dataViews) ); diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx index 0055e24685b8..12cd26df28f1 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx @@ -11,7 +11,6 @@ import 'react-grid-layout/css/styles.css'; import { pick } from 'lodash'; import classNames from 'classnames'; -import { useEffectOnce } from 'react-use/lib'; import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'; @@ -31,19 +30,20 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { const viewMode = dashboard.select((state) => state.explicitInput.viewMode); const useMargins = dashboard.select((state) => state.explicitInput.useMargins); const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId); + const animatePanelTransforms = dashboard.select( + (state) => state.componentState.animatePanelTransforms + ); - // turn off panel transform animations for the first 500ms so that the dashboard doesn't animate on its first render. - const [animatePanelTransforms, setAnimatePanelTransforms] = useState(false); - useEffectOnce(() => { - setTimeout(() => setAnimatePanelTransforms(true), 500); - }); - + /** + * Track panel maximized state delayed by one tick and use it to prevent + * panel sliding animations on maximize and minimize. + */ + const [delayedIsPanelExpanded, setDelayedIsPanelMaximized] = useState(false); useEffect(() => { if (expandedPanelId) { - setAnimatePanelTransforms(false); + setDelayedIsPanelMaximized(true); } else { - // delaying enabling CSS transforms to the next tick prevents a panel slide animation on minimize - setTimeout(() => setAnimatePanelTransforms(true), 0); + setTimeout(() => setDelayedIsPanelMaximized(false), 0); } }, [expandedPanelId]); @@ -107,7 +107,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { 'dshLayout-withoutMargins': !useMargins, 'dshLayout--viewing': viewMode === ViewMode.VIEW, 'dshLayout--editing': viewMode !== ViewMode.VIEW, - 'dshLayout--noAnimation': !animatePanelTransforms || expandedPanelId, + 'dshLayout--noAnimation': !animatePanelTransforms || delayedIsPanelExpanded, 'dshLayout-isMaximizedPanel': expandedPanelId !== undefined, }); diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx index d3dea0d25003..5a8024471aed 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx @@ -102,7 +102,6 @@ export function runSaveAs(this: DashboardContainer) { this.dispatch.setLastSavedInput(stateToSave); }); } - if (newCopyOnSave || !lastSavedId) this.expectIdChange(); resolve(saveResult); return saveResult; }; @@ -175,10 +174,7 @@ export async function runClone(this: DashboardContainer) { saveOptions: { saveAsCopy: true }, currentState: { ...currentState, title: newTitle }, }); - - this.dispatch.setTitle(newTitle); resolve(saveResult); - this.expectIdChange(); return saveResult.id ? { id: saveResult.id } : { error: saveResult.error }; }; showCloneModal({ onClone, title: currentState.title, onClose: () => resolve(undefined) }); diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/controls/dashboard_control_group_integration.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/controls/dashboard_control_group_integration.ts index c9a8bf73222a..7c907461d8f8 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/controls/dashboard_control_group_integration.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/controls/dashboard_control_group_integration.ts @@ -50,7 +50,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) { chainingSystem: deepEqual, ignoreParentSettings: deepEqual, }; - this.subscriptions.add( + this.integrationSubscriptions.add( this.controlGroup .getInput$() .pipe( @@ -83,7 +83,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) { }; // pass down any pieces of input needed to refetch or force refetch data for the controls - this.subscriptions.add( + this.integrationSubscriptions.add( (this.getInput$() as Readonly>) .pipe( distinctUntilChanged((a, b) => @@ -106,7 +106,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) { ); // when control group outputs filters, force a refresh! - this.subscriptions.add( + this.integrationSubscriptions.add( this.controlGroup .getOutput$() .pipe( @@ -118,7 +118,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) { .subscribe(() => this.forceRefresh(false)) // we should not reload the control group when the control group output changes - otherwise, performance is severely impacted ); - this.subscriptions.add( + this.integrationSubscriptions.add( this.controlGroup .getOutput$() .pipe( @@ -134,7 +134,7 @@ export function startSyncingDashboardControlGroup(this: DashboardContainer) { ); // the Control Group needs to know when any dashboard children are loading in order to know when to move on to the next time slice when playing. - this.subscriptions.add( + this.integrationSubscriptions.add( this.getAnyChildOutputChange$().subscribe(() => { if (!this.controlGroup) { return; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts index 529d91a284e4..4bc188c69a2c 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts @@ -30,14 +30,12 @@ import { pluginServices } from '../../../services/plugin_services'; import { DashboardCreationOptions } from '../dashboard_container_factory'; import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants'; -const embeddableId = 'create-dat-dashboard'; - test('throws error when no data views are available', async () => { pluginServices.getServices().data.dataViews.getDefaultDataView = jest .fn() .mockReturnValue(undefined); await expect(async () => { - await createDashboard(embeddableId); + await createDashboard(); }).rejects.toThrow('Dashboard requires at least one data view before it can be initialized.'); // reset get default data view @@ -49,7 +47,7 @@ test('throws error when provided validation function returns invalid', async () validateLoadedSavedObject: jest.fn().mockImplementation(() => false), }; await expect(async () => { - await createDashboard(embeddableId, creationOptions, 0, 'test-id'); + await createDashboard(creationOptions, 0, 'test-id'); }).rejects.toThrow('Dashboard failed saved object result validation'); }); @@ -62,7 +60,7 @@ test('pulls state from dashboard saved object when given a saved object id', asy description: `wow would you look at that? Wow.`, }, }); - const dashboard = await createDashboard(embeddableId, {}, 0, 'wow-such-id'); + const dashboard = await createDashboard({}, 0, 'wow-such-id'); expect( pluginServices.getServices().dashboardSavedObject.loadDashboardStateFromSavedObject ).toHaveBeenCalledWith({ id: 'wow-such-id' }); @@ -81,12 +79,7 @@ test('pulls state from session storage which overrides state from saved object', pluginServices.getServices().dashboardSessionStorage.getState = jest .fn() .mockReturnValue({ description: 'wow this description marginally better' }); - const dashboard = await createDashboard( - embeddableId, - { useSessionStorageIntegration: true }, - 0, - 'wow-such-id' - ); + const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id'); expect(dashboard.getState().explicitInput.description).toBe( 'wow this description marginally better' ); @@ -105,10 +98,9 @@ test('pulls state from creation options initial input which overrides all other .fn() .mockReturnValue({ description: 'wow this description marginally better' }); const dashboard = await createDashboard( - embeddableId, { useSessionStorageIntegration: true, - initialInput: { description: 'wow this description is a masterpiece' }, + getInitialInput: () => ({ description: 'wow this description is a masterpiece' }), }, 0, 'wow-such-id' @@ -123,12 +115,12 @@ test('applies filters and query from state to query service', async () => { { meta: { alias: 'test', disabled: false, negate: false, index: 'test' } }, ]; const query = { language: 'kql', query: 'query' }; - await createDashboard(embeddableId, { + await createDashboard({ useUnifiedSearchIntegration: true, unifiedSearchSettings: { kbnUrlStateStorage: createKbnUrlStateStorage(), }, - initialInput: { filters, query }, + getInitialInput: () => ({ filters, query }), }); expect(pluginServices.getServices().data.query.queryString.setQuery).toHaveBeenCalledWith(query); expect(pluginServices.getServices().data.query.filterManager.setAppFilters).toHaveBeenCalledWith( @@ -139,12 +131,12 @@ test('applies filters and query from state to query service', async () => { test('applies time range and refresh interval from initial input to query service if time restore is on', async () => { const timeRange = { from: new Date().toISOString(), to: new Date().toISOString() }; const refreshInterval = { pause: false, value: 42 }; - await createDashboard(embeddableId, { + await createDashboard({ useUnifiedSearchIntegration: true, unifiedSearchSettings: { kbnUrlStateStorage: createKbnUrlStateStorage(), }, - initialInput: { timeRange, refreshInterval, timeRestore: true }, + getInitialInput: () => ({ timeRange, refreshInterval, timeRestore: true }), }); expect( pluginServices.getServices().data.query.timefilter.timefilter.setTime @@ -159,7 +151,7 @@ test('applied time range from query service to initial input if time restore is pluginServices.getServices().data.query.timefilter.timefilter.getTime = jest .fn() .mockReturnValue(timeRange); - const dashboard = await createDashboard(embeddableId, { + const dashboard = await createDashboard({ useUnifiedSearchIntegration: true, unifiedSearchSettings: { kbnUrlStateStorage: createKbnUrlStateStorage(), @@ -177,9 +169,9 @@ test('replaces panel with incoming embeddable if id matches existing panel', asy } as ContactCardEmbeddableInput, embeddableId: 'i_match', }; - const dashboard = await createDashboard(embeddableId, { - incomingEmbeddable, - initialInput: { + const dashboard = await createDashboard({ + getIncomingEmbeddable: () => incomingEmbeddable, + getInitialInput: () => ({ panels: { i_match: getSampleDashboardPanel({ explicitInput: { @@ -189,7 +181,7 @@ test('replaces panel with incoming embeddable if id matches existing panel', asy type: CONTACT_CARD_EMBEDDABLE, }), }, - }, + }), }); expect(dashboard.getState().explicitInput.panels.i_match.explicitInput).toStrictEqual( expect.objectContaining({ @@ -216,9 +208,9 @@ test('creates new embeddable with incoming embeddable if id does not match exist .fn() .mockReturnValue(mockContactCardFactory); - await createDashboard(embeddableId, { - incomingEmbeddable, - initialInput: { + await createDashboard({ + getIncomingEmbeddable: () => incomingEmbeddable, + getInitialInput: () => ({ panels: { i_do_not_match: getSampleDashboardPanel({ explicitInput: { @@ -228,7 +220,7 @@ test('creates new embeddable with incoming embeddable if id does not match exist type: CONTACT_CARD_EMBEDDABLE, }), }, - }, + }), }); // flush promises @@ -258,11 +250,11 @@ test('creates a control group from the control group factory and waits for it to pluginServices.getServices().embeddable.getEmbeddableFactory = jest .fn() .mockReturnValue(mockControlGroupFactory); - await createDashboard(embeddableId, { + await createDashboard({ useControlGroupIntegration: true, - initialInput: { + getInitialInput: () => ({ controlGroupInput: { controlStyle: 'twoLine' } as unknown as ControlGroupInput, - }, + }), }); // flush promises await new Promise((r) => setTimeout(r, 1)); @@ -302,7 +294,7 @@ test('searchSessionId is updated prior to child embeddable parent subscription e sessionCount++; return `searchSessionId${sessionCount}`; }; - const dashboard = await createDashboard(embeddableId, { + const dashboard = await createDashboard({ searchSessionSettings: { getSearchSessionIdFromURL: () => undefined, removeSessionIdFromUrl: () => {}, diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index ef810f025b84..58983196ea23 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -27,47 +27,21 @@ import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state'; import { startSyncingDashboardControlGroup } from './controls/dashboard_control_group_integration'; import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration'; +import { LoadDashboardFromSavedObjectReturn } from '../../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object'; /** - * - * @param creationOptions + * Builds a new Dashboard from scratch. */ export const createDashboard = async ( - embeddableId: string, creationOptions?: DashboardCreationOptions, dashboardCreationStartTime?: number, savedObjectId?: string ): Promise => { - // -------------------------------------------------------------------------------------- - // Unpack services & Options - // -------------------------------------------------------------------------------------- const { - dashboardSessionStorage, - embeddable: { getEmbeddableFactory }, - data: { - dataViews, - query: queryService, - search: { session }, - }, + data: { dataViews }, dashboardSavedObject: { loadDashboardStateFromSavedObject }, } = pluginServices.getServices(); - const { - queryString, - filterManager, - timefilter: { timefilter: timefilterService }, - } = queryService; - - const { - searchSessionSettings, - unifiedSearchSettings, - validateLoadedSavedObject, - useControlGroupIntegration, - useUnifiedSearchIntegration, - initialInput: overrideInput, - useSessionStorageIntegration, - } = creationOptions ?? {}; - // -------------------------------------------------------------------------------------- // Create method which allows work to be done on the dashboard container when it's ready. // -------------------------------------------------------------------------------------- @@ -83,12 +57,9 @@ export const createDashboard = async ( // -------------------------------------------------------------------------------------- // Lazy load required systems and Dashboard saved object. // -------------------------------------------------------------------------------------- - const reduxEmbeddablePackagePromise = lazyLoadReduxToolsPackage(); const defaultDataViewAssignmentPromise = dataViews.getDefaultDataView(); - const dashboardSavedObjectPromise = savedObjectId - ? loadDashboardStateFromSavedObject({ id: savedObjectId }) - : Promise.resolve(undefined); + const dashboardSavedObjectPromise = loadDashboardStateFromSavedObject({ id: savedObjectId }); const [reduxEmbeddablePackage, savedObjectResult, defaultDataView] = await Promise.all([ reduxEmbeddablePackagePromise, @@ -96,17 +67,82 @@ export const createDashboard = async ( defaultDataViewAssignmentPromise, ]); - // -------------------------------------------------------------------------------------- - // Run validations. - // -------------------------------------------------------------------------------------- if (!defaultDataView) { throw new Error('Dashboard requires at least one data view before it can be initialized.'); } + // -------------------------------------------------------------------------------------- + // Initialize Dashboard integrations + // -------------------------------------------------------------------------------------- + const { input, searchSessionId } = await initializeDashboard({ + loadDashboardReturn: savedObjectResult, + untilDashboardReady, + creationOptions, + }); + + // -------------------------------------------------------------------------------------- + // Build and return the dashboard container. + // -------------------------------------------------------------------------------------- + const dashboardContainer = new DashboardContainer( + input, + reduxEmbeddablePackage, + searchSessionId, + savedObjectResult?.dashboardInput, + dashboardCreationStartTime, + undefined, + creationOptions, + savedObjectId + ); + dashboardContainerReady$.next(dashboardContainer); + return dashboardContainer; +}; + +/** + * Initializes a Dashboard and starts all of its integrations + */ +export const initializeDashboard = async ({ + loadDashboardReturn, + untilDashboardReady, + creationOptions, + controlGroup, +}: { + loadDashboardReturn: LoadDashboardFromSavedObjectReturn; + untilDashboardReady: () => Promise; + creationOptions?: DashboardCreationOptions; + controlGroup?: ControlGroupContainer; +}) => { + const { + dashboardSessionStorage, + embeddable: { getEmbeddableFactory }, + data: { + query: queryService, + search: { session }, + }, + } = pluginServices.getServices(); + const { + queryString, + filterManager, + timefilter: { timefilter: timefilterService }, + } = queryService; + + const { + getInitialInput, + searchSessionSettings, + unifiedSearchSettings, + validateLoadedSavedObject, + useControlGroupIntegration, + useUnifiedSearchIntegration, + useSessionStorageIntegration, + } = creationOptions ?? {}; + const overrideInput = getInitialInput?.(); + + // -------------------------------------------------------------------------------------- + // Run validation. + // -------------------------------------------------------------------------------------- if ( - savedObjectResult && + loadDashboardReturn && validateLoadedSavedObject && - !validateLoadedSavedObject(savedObjectResult) + !validateLoadedSavedObject(loadDashboardReturn) ) { throw new Error('Dashboard failed saved object result validation'); } @@ -116,7 +152,7 @@ export const createDashboard = async ( // -------------------------------------------------------------------------------------- const sessionStorageInput = ((): Partial | undefined => { if (!useSessionStorageIntegration) return; - return dashboardSessionStorage.getState(savedObjectId); + return dashboardSessionStorage.getState(loadDashboardReturn.dashboardId); })(); // -------------------------------------------------------------------------------------- @@ -124,10 +160,9 @@ export const createDashboard = async ( // -------------------------------------------------------------------------------------- const initialInput: DashboardContainerInput = cloneDeep({ ...DEFAULT_DASHBOARD_INPUT, - ...(savedObjectResult?.dashboardInput ?? {}), + ...(loadDashboardReturn?.dashboardInput ?? {}), ...sessionStorageInput, ...overrideInput, - id: embeddableId, }); initialInput.executionContext = { @@ -166,8 +201,7 @@ export const createDashboard = async ( } untilDashboardReady().then((dashboardContainer) => { - const stopSyncingUnifiedSearchState = - syncUnifiedSearchState.bind(dashboardContainer)(kbnUrlStateStorage); + const stopSyncingUnifiedSearchState = syncUnifiedSearchState.bind(dashboardContainer)(); dashboardContainer.stopSyncingWithUnifiedSearch = () => { stopSyncingUnifiedSearchState(); stopSyncingQueryServiceStateWithUrl(); @@ -178,16 +212,20 @@ export const createDashboard = async ( // -------------------------------------------------------------------------------------- // Place the incoming embeddable if there is one // -------------------------------------------------------------------------------------- - const incomingEmbeddable = creationOptions?.incomingEmbeddable; + const incomingEmbeddable = creationOptions?.getIncomingEmbeddable?.(); if (incomingEmbeddable) { - initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable. + const scrolltoIncomingEmbeddable = (container: DashboardContainer, id: string) => { + container.setScrollToPanelId(id); + container.setHighlightPanelId(id); + }; - const panelExists = + initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable. + if ( incomingEmbeddable.embeddableId && - Boolean(initialInput.panels[incomingEmbeddable.embeddableId]); - if (panelExists) { + Boolean(initialInput.panels[incomingEmbeddable.embeddableId]) + ) { // this embeddable already exists, we will update the explicit input. - const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId as string]; + const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId]; const sameType = panelToUpdate.type === incomingEmbeddable.type; panelToUpdate.type = incomingEmbeddable.type; @@ -196,22 +234,24 @@ export const createDashboard = async ( ...(sameType ? panelToUpdate.explicitInput : {}), ...incomingEmbeddable.input, - id: incomingEmbeddable.embeddableId as string, + id: incomingEmbeddable.embeddableId, // maintain hide panel titles setting. hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles, }; + untilDashboardReady().then((container) => + scrolltoIncomingEmbeddable(container, incomingEmbeddable.embeddableId as string) + ); } else { // otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created. untilDashboardReady().then(async (container) => { - container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input); + const embeddable = await container.addNewEmbeddable( + incomingEmbeddable.type, + incomingEmbeddable.input + ); + scrolltoIncomingEmbeddable(container, embeddable.id); }); } - - untilDashboardReady().then(async (container) => { - container.setScrollToPanelId(incomingEmbeddable.embeddableId); - container.setHighlightPanelId(incomingEmbeddable.embeddableId); - }); } // -------------------------------------------------------------------------------------- @@ -251,7 +291,7 @@ export const createDashboard = async ( ControlGroupContainer >(CONTROL_GROUP_TYPE); const { filters, query, timeRange, viewMode, controlGroupInput, id } = initialInput; - const controlGroup = await controlsGroupFactory?.create({ + const fullControlGroupInput = { id: `control_group_${id ?? 'new_dashboard'}`, ...getDefaultControlGroupInput(), ...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults @@ -259,9 +299,15 @@ export const createDashboard = async ( viewMode, filters, query, - }); - if (!controlGroup || isErrorEmbeddable(controlGroup)) { - throw new Error('Error in control group startup'); + }; + if (controlGroup) { + controlGroup.updateInputAndReinitialize(fullControlGroupInput); + } else { + const newControlGroup = await controlsGroupFactory?.create(fullControlGroupInput); + if (!newControlGroup || isErrorEmbeddable(newControlGroup)) { + throw new Error('Error in control group startup'); + } + controlGroup = newControlGroup; } untilDashboardReady().then((dashboardContainer) => { @@ -275,22 +321,17 @@ export const createDashboard = async ( // Start the data views integration. // -------------------------------------------------------------------------------------- untilDashboardReady().then((dashboardContainer) => { - dashboardContainer.subscriptions.add(startSyncingDashboardDataViews.bind(dashboardContainer)()); + dashboardContainer.integrationSubscriptions.add( + startSyncingDashboardDataViews.bind(dashboardContainer)() + ); }); // -------------------------------------------------------------------------------------- - // Build and return the dashboard container. + // Start animating panel transforms 500 ms after dashboard is created. // -------------------------------------------------------------------------------------- - const dashboardContainer = new DashboardContainer( - initialInput, - reduxEmbeddablePackage, - initialSearchSessionId, - savedObjectResult?.dashboardInput, - dashboardCreationStartTime, - undefined, - creationOptions, - savedObjectId + untilDashboardReady().then((dashboard) => + setTimeout(() => dashboard.dispatch.setAnimatePanelTransforms(true), 500) ); - dashboardContainerReady$.next(dashboardContainer); - return dashboardContainer; + + return { input: initialInput, searchSessionId: initialSearchSessionId }; }; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/search_sessions/start_dashboard_search_session_integration.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/search_sessions/start_dashboard_search_session_integration.ts index 7f59b56c228b..7d74e91ad6c9 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/search_sessions/start_dashboard_search_session_integration.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/search_sessions/start_dashboard_search_session_integration.ts @@ -86,5 +86,5 @@ export function startDashboardSearchSessionIntegration( } }); - this.subscriptions.add(searchSessionIdChangeSubscription); + this.integrationSubscriptions.add(searchSessionIdChangeSubscription); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/unified_search/sync_dashboard_unified_search_state.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/unified_search/sync_dashboard_unified_search_state.ts index 02095015f75a..43220de9b69e 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/unified_search/sync_dashboard_unified_search_state.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/unified_search/sync_dashboard_unified_search_state.ts @@ -10,7 +10,6 @@ import { Subject } from 'rxjs'; import { distinctUntilChanged, finalize, switchMap, tap } from 'rxjs/operators'; import type { Filter, Query } from '@kbn/es-query'; -import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public'; import { connectToQueryState, waitUntilNextSessionCompletes$ } from '@kbn/data-plugin/public'; @@ -21,10 +20,7 @@ import { pluginServices } from '../../../../services/plugin_services'; * Sets up syncing and subscriptions between the filter state from the Data plugin * and the dashboard Redux store. */ -export function syncUnifiedSearchState( - this: DashboardContainer, - kbnUrlStateStorage: IKbnUrlStateStorage -) { +export function syncUnifiedSearchState(this: DashboardContainer) { const { data: { query: queryService, search }, } = pluginServices.getServices(); diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 1360fa7737c1..d6278d478796 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import React, { createContext, useContext } from 'react'; import ReactDOM from 'react-dom'; +import { batch } from 'react-redux'; import { Subject, Subscription } from 'rxjs'; +import React, { createContext, useContext } from 'react'; import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public'; import { @@ -20,10 +21,10 @@ import { type EmbeddableFactory, } from '@kbn/embeddable-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; +import { RefreshInterval } from '@kbn/data-plugin/public'; import type { Filter, TimeRange, Query } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/public'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; -import type { RefreshInterval } from '@kbn/data-plugin/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { ControlGroupContainer } from '@kbn/controls-plugin/public'; import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public'; @@ -44,6 +45,7 @@ import { import { DASHBOARD_CONTAINER_TYPE } from '../..'; import { createPanelState } from '../component/panel'; import { pluginServices } from '../../services/plugin_services'; +import { initializeDashboard } from './create/create_dashboard'; import { DashboardCreationOptions } from './dashboard_container_factory'; import { DashboardAnalyticsService } from '../../services/analytics/types'; import { DashboardViewport } from '../component/viewport/dashboard_viewport'; @@ -93,7 +95,8 @@ export class DashboardContainer extends Container { - this.expectingIdChange = false; - }, 1); // turn this off after the next update. - } - public runClone = runClone; public runSaveAs = runSaveAs; public runQuickSave = runQuickSave; @@ -361,6 +346,49 @@ export class DashboardContainer extends Container + ) => { + this.integrationSubscriptions.unsubscribe(); + this.integrationSubscriptions = new Subscription(); + this.stopSyncingWithUnifiedSearch?.(); + + const { + dashboardSavedObject: { loadDashboardStateFromSavedObject }, + } = pluginServices.getServices(); + if (newCreationOptions) { + this.creationOptions = { ...this.creationOptions, ...newCreationOptions }; + } + const loadDashboardReturn = await loadDashboardStateFromSavedObject({ id: newSavedObjectId }); + + const dashboardContainerReady$ = new Subject(); + const untilDashboardReady = () => + new Promise((resolve) => { + const subscription = dashboardContainerReady$.subscribe((container) => { + subscription.unsubscribe(); + resolve(container); + }); + }); + + const { input: newInput, searchSessionId } = await initializeDashboard({ + creationOptions: this.creationOptions, + controlGroup: this.controlGroup, + untilDashboardReady, + loadDashboardReturn, + }); + + this.searchSessionId = searchSessionId; + + this.updateInput(newInput); + batch(() => { + this.dispatch.setLastSavedInput(loadDashboardReturn?.dashboardInput); + this.dispatch.setAnimatePanelTransforms(false); // prevents panels from animating on navigate. + this.dispatch.setLastSavedId(newSavedObjectId); + }); + dashboardContainerReady$.next(this); + }; + /** * Gets all the dataviews that are actively being used in the dashboard * @returns An array of dataviews diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container_factory.tsx index 2af0d60572cb..e86ecce4d23e 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container_factory.tsx @@ -34,9 +34,9 @@ export type DashboardContainerFactory = EmbeddableFactory< >; export interface DashboardCreationOptions { - initialInput?: Partial; + getInitialInput?: () => Partial; - incomingEmbeddable?: EmbeddablePackageState; + getIncomingEmbeddable?: () => EmbeddablePackageState | undefined; useSearchSessionsIntegration?: boolean; searchSessionSettings?: { @@ -98,7 +98,7 @@ export class DashboardContainerFactoryDefinition const { createDashboard } = await import('./create/create_dashboard'); try { return Promise.resolve( - createDashboard(initialInput.id, creationOptions, dashboardCreationStartTime, savedObjectId) + createDashboard(creationOptions, dashboardCreationStartTime, savedObjectId) ); } catch (e) { return new ErrorEmbeddable(e.text, { id: e.id }); diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx index 4497812c4c71..415e62c1953f 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx @@ -26,7 +26,7 @@ describe('dashboard renderer', () => { mockDashboardContainer = { destroy: jest.fn(), render: jest.fn(), - isExpectingIdChange: jest.fn().mockReturnValue(false), + navigateToDashboard: jest.fn(), } as unknown as DashboardContainer; mockDashboardFactory = { create: jest.fn().mockReturnValue(mockDashboardContainer), @@ -77,7 +77,7 @@ describe('dashboard renderer', () => { expect(mockDashboardContainer.destroy).toHaveBeenCalledTimes(1); }); - test('destroys dashboard container on unexpected ID change', async () => { + test('calls navigate and does not destroy dashboard container on ID change', async () => { let wrapper: ReactWrapper; await act(async () => { wrapper = await mountWithIntl(); @@ -85,18 +85,9 @@ describe('dashboard renderer', () => { await act(async () => { await wrapper.setProps({ savedObjectId: 'saved_object_kibanakiwi' }); }); - expect(mockDashboardContainer.destroy).toHaveBeenCalledTimes(1); - }); - - test('does not destroy dashboard container on expected ID change', async () => { - let wrapper: ReactWrapper; - await act(async () => { - wrapper = await mountWithIntl(); - }); - mockDashboardContainer.isExpectingIdChange = jest.fn().mockReturnValue(true); - await act(async () => { - await wrapper.setProps({ savedObjectId: 'saved_object_kibanakiwi' }); - }); expect(mockDashboardContainer.destroy).not.toHaveBeenCalled(); + expect(mockDashboardContainer.navigateToDashboard).toHaveBeenCalledWith( + 'saved_object_kibanakiwi' + ); }); }); diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx index 04d85c2f06b9..aff31252af04 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx @@ -47,7 +47,6 @@ export const DashboardRenderer = forwardRef(); - const [dashboardIdToBuild, setDashboardIdToBuild] = useState(savedObjectId); useImperativeHandle( ref, @@ -67,9 +66,10 @@ export const DashboardRenderer = forwardRef { - // check if dashboard container is expecting id change... if not, update dashboardIdToBuild to force it to rebuild the container. if (!dashboardContainer) return; - if (!dashboardContainer.isExpectingIdChange()) setDashboardIdToBuild(savedObjectId); + + // When a dashboard already exists, don't rebuild it, just set a new id. + dashboardContainer.navigateToDashboard(savedObjectId); // Disabling exhaustive deps because this useEffect should only be triggered when the savedObjectId changes. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -115,9 +115,9 @@ export const DashboardRenderer = forwardRef) => { + state.componentState.lastSavedId = action.payload; + }, + setStateFromSettingsFlyout: ( state: DashboardReduxState, action: PayloadAction @@ -218,4 +222,11 @@ export const dashboardContainerReducers = { setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction) => { state.componentState.highlightPanelId = action.payload; }, + + setAnimatePanelTransforms: ( + state: DashboardReduxState, + action: PayloadAction + ) => { + state.componentState.animatePanelTransforms = action.payload; + }, }; diff --git a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts index 897ac529fe61..040acd2087df 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts @@ -84,7 +84,7 @@ export function startDiffingDashboardState( creationOptions?: DashboardCreationOptions ) { const checkForUnsavedChangesSubject$ = new Subject(); - this.subscriptions.add( + this.diffingSubscription.add( checkForUnsavedChangesSubject$ .pipe( startWith(null), diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index 544317d9f6bc..bdd7b7083a6d 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -26,6 +26,7 @@ export type DashboardStateFromSettingsFlyout = DashboardStateFromSaveModal & Das export interface DashboardPublicState { lastSavedInput: DashboardContainerInput; + animatePanelTransforms?: boolean; isEmbeddedExternally?: boolean; hasUnsavedChanges?: boolean; hasOverlays?: boolean; diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index c68b6deb4695..30ac6c275ddd 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -94,7 +94,7 @@ export class DashboardAddPanelService extends FtrService { continue; } await button.click(); - await this.common.closeToast(); + await this.common.closeToastIfExists(); embeddableList.push(name); } }); diff --git a/x-pack/plugins/security_solution/public/common/components/dashboards/dashboard_renderer.tsx b/x-pack/plugins/security_solution/public/common/components/dashboards/dashboard_renderer.tsx index 1f4cdff815df..316301b0244b 100644 --- a/x-pack/plugins/security_solution/public/common/components/dashboards/dashboard_renderer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/dashboards/dashboard_renderer.tsx @@ -44,7 +44,7 @@ const DashboardRendererComponent = ({ const getCreationOptions = useCallback( () => Promise.resolve({ - initialInput: { timeRange, viewMode: ViewMode.VIEW, query, filters }, + getInitialInput: () => ({ timeRange, viewMode: ViewMode.VIEW, query, filters }), }), [filters, query, timeRange] );