diff --git a/src/plugins/dashboard/common/dashboard_container/types.ts b/src/plugins/dashboard/common/dashboard_container/types.ts index be8ccb4797ca3d..bcb7670f18e12e 100644 --- a/src/plugins/dashboard/common/dashboard_container/types.ts +++ b/src/plugins/dashboard/common/dashboard_container/types.ts @@ -59,7 +59,6 @@ export interface DashboardContainerInput extends EmbeddableInput { tags: string[]; viewMode: ViewMode; description?: string; - isEmbeddedExternally?: boolean; executionContext: KibanaExecutionContext; // dashboard options: TODO, build a new system to avoid all shared state appearing here. See https://github.com/elastic/kibana/issues/144532 for more information. diff --git a/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts new file mode 100644 index 00000000000000..3db6129c7c6f90 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; +import type { DashboardContainerInput } from '../../common'; +import { initializeTrackPanel } from './track_panel'; +import { initializeTrackOverlay } from './track_overlay'; +import { initializeUnsavedChanges } from './unsaved_changes'; + +export interface InitialComponentState { + anyMigrationRun: boolean; + isEmbeddedExternally: boolean; + lastSavedInput: DashboardContainerInput; + lastSavedId: string | undefined; + managed: boolean; +} + +export function getDashboardApi( + initialComponentState: InitialComponentState, + untilEmbeddableLoaded: (id: string) => Promise +) { + const animatePanelTransforms$ = new BehaviorSubject(false); // set panel transforms to false initially to avoid panels animating on initial render. + const fullScreenMode$ = new BehaviorSubject(false); + const managed$ = new BehaviorSubject(initialComponentState.managed); + const savedObjectId$ = new BehaviorSubject(initialComponentState.lastSavedId); + + const trackPanel = initializeTrackPanel(untilEmbeddableLoaded); + + return { + ...trackPanel, + ...initializeTrackOverlay(trackPanel.setFocusedPanelId), + ...initializeUnsavedChanges( + initialComponentState.anyMigrationRun, + initialComponentState.lastSavedInput + ), + animatePanelTransforms$, + fullScreenMode$, + isEmbeddedExternally: initialComponentState.isEmbeddedExternally, + managed$, + savedObjectId: savedObjectId$, + setAnimatePanelTransforms: (animate: boolean) => animatePanelTransforms$.next(animate), + setFullScreenMode: (fullScreenMode: boolean) => fullScreenMode$.next(fullScreenMode), + setManaged: (managed: boolean) => managed$.next(managed), + setSavedObjectId: (id: string | undefined) => savedObjectId$.next(id), + }; +} diff --git a/src/plugins/dashboard/public/dashboard_api/track_overlay.ts b/src/plugins/dashboard/public/dashboard_api/track_overlay.ts new file mode 100644 index 00000000000000..7a417654a9a0c2 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/track_overlay.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { OverlayRef } from '@kbn/core-mount-utils-browser'; +import { BehaviorSubject } from 'rxjs'; + +export function initializeTrackOverlay(setFocusedPanelId: (id: string | undefined) => void) { + let overlayRef: OverlayRef; + const hasOverlays$ = new BehaviorSubject(false); + + function clearOverlays() { + hasOverlays$.next(false); + setFocusedPanelId(undefined); + overlayRef?.close(); + } + + return { + clearOverlays, + hasOverlays$, + openOverlay: (ref: OverlayRef, options?: { focusedPanelId?: string }) => { + clearOverlays(); + hasOverlays$.next(true); + overlayRef = ref; + if (options?.focusedPanelId) { + setFocusedPanelId(options.focusedPanelId); + } + }, + }; +} diff --git a/src/plugins/dashboard/public/dashboard_api/track_panel.ts b/src/plugins/dashboard/public/dashboard_api/track_panel.ts new file mode 100644 index 00000000000000..3f3a9b4aaad4bc --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/track_panel.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; + +export function initializeTrackPanel(untilEmbeddableLoaded: (id: string) => Promise) { + const expandedPanelId$ = new BehaviorSubject(undefined); + const focusedPanelId$ = new BehaviorSubject(undefined); + const highlightPanelId$ = new BehaviorSubject(undefined); + const scrollToPanelId$ = new BehaviorSubject(undefined); + let scrollPosition: number | undefined; + + function setScrollToPanelId(id: string | undefined) { + if (scrollToPanelId$.value !== id) scrollToPanelId$.next(id); + } + + function setExpandedPanelId(id: string | undefined) { + if (expandedPanelId$.value !== id) expandedPanelId$.next(id); + } + + return { + expandedPanelId: expandedPanelId$, + expandPanel: (panelId: string) => { + const isPanelExpanded = Boolean(expandedPanelId$.value); + + if (isPanelExpanded) { + setExpandedPanelId(undefined); + setScrollToPanelId(panelId); + return; + } + + setExpandedPanelId(panelId); + if (window.scrollY > 0) { + scrollPosition = window.scrollY; + } + }, + focusedPanelId$, + highlightPanelId$, + highlightPanel: (panelRef: HTMLDivElement) => { + const id = highlightPanelId$.value; + + if (id && panelRef) { + untilEmbeddableLoaded(id).then(() => { + panelRef.classList.add('dshDashboardGrid__item--highlighted'); + // Removes the class after the highlight animation finishes + setTimeout(() => { + panelRef.classList.remove('dshDashboardGrid__item--highlighted'); + }, 5000); + }); + } + highlightPanelId$.next(undefined); + }, + scrollToPanelId$, + scrollToPanel: async (panelRef: HTMLDivElement) => { + const id = scrollToPanelId$.value; + if (!id) return; + + untilEmbeddableLoaded(id).then(() => { + setScrollToPanelId(undefined); + if (scrollPosition) { + panelRef.ontransitionend = () => { + // Scroll to the last scroll position after the transition ends to ensure the panel is back in the right position before scrolling + // This is necessary because when an expanded panel collapses, it takes some time for the panel to return to its original position + window.scrollTo({ top: scrollPosition }); + scrollPosition = undefined; + panelRef.ontransitionend = null; + }; + return; + } + + panelRef.scrollIntoView({ block: 'center' }); + }); + }, + scrollToTop: () => { + window.scroll(0, 0); + }, + setExpandedPanelId, + setFocusedPanelId: (id: string | undefined) => { + if (focusedPanelId$.value !== id) focusedPanelId$.next(id); + setScrollToPanelId(id); + }, + setHighlightPanelId: (id: string | undefined) => { + if (highlightPanelId$.value !== id) highlightPanelId$.next(id); + }, + setScrollToPanelId, + }; +} diff --git a/src/plugins/dashboard/public/dashboard_api/types.ts b/src/plugins/dashboard/public/dashboard_api/types.ts index bdd72afe9dc406..c874ff9e672418 100644 --- a/src/plugins/dashboard/public/dashboard_api/types.ts +++ b/src/plugins/dashboard/public/dashboard_api/types.ts @@ -48,23 +48,23 @@ export type DashboardApi = CanExpandPanels & PublishesViewMode & TracksOverlays & { addFromLibrary: () => void; - animatePanelTransforms$: PublishingSubject; + animatePanelTransforms$: PublishingSubject; asyncResetToLastSavedState: () => Promise; controlGroupApi$: PublishingSubject; - embeddedExternally$: PublishingSubject; - fullScreenMode$: PublishingSubject; + fullScreenMode$: PublishingSubject; focusedPanelId$: PublishingSubject; forceRefresh: () => void; getRuntimeStateForControlGroup: () => UnsavedPanelState | undefined; getSerializedStateForControlGroup: () => SerializedPanelState; getSettings: () => DashboardStateFromSettingsFlyout; getDashboardPanelFromId: (id: string) => Promise; - hasOverlays$: PublishingSubject; - hasRunMigrations$: PublishingSubject; - hasUnsavedChanges$: PublishingSubject; + hasOverlays$: PublishingSubject; + hasRunMigrations$: PublishingSubject; + hasUnsavedChanges$: PublishingSubject; highlightPanel: (panelRef: HTMLDivElement) => void; highlightPanelId$: PublishingSubject; - managed$: PublishingSubject; + isEmbeddedExternally: boolean; + managed$: PublishingSubject; panels$: PublishingSubject; registerChildApi: (api: DefaultEmbeddableApi) => void; runInteractiveSave: (interactionMode: ViewMode) => Promise; diff --git a/src/plugins/dashboard/public/dashboard_api/unsaved_changes.ts b/src/plugins/dashboard/public/dashboard_api/unsaved_changes.ts new file mode 100644 index 00000000000000..af588081ecc877 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/unsaved_changes.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; +import type { DashboardContainerInput } from '../../common'; + +export function initializeUnsavedChanges( + anyMigrationRun: boolean, + lastSavedInput: DashboardContainerInput +) { + const hasRunMigrations$ = new BehaviorSubject(anyMigrationRun); + const hasUnsavedChanges$ = new BehaviorSubject(false); + const lastSavedInput$ = new BehaviorSubject(lastSavedInput); + + return { + hasRunMigrations$, + hasUnsavedChanges$, + lastSavedInput$, + setHasUnsavedChanges: (hasUnsavedChanges: boolean) => + hasUnsavedChanges$.next(hasUnsavedChanges), + setLastSavedInput: (input: DashboardContainerInput) => { + lastSavedInput$.next(input); + + // if we set the last saved input, it means we have saved this Dashboard - therefore clientside migrations have + // been serialized into the SO. + hasRunMigrations$.next(false); + }, + }; +} diff --git a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts index 5ec8a0716d410a..2845808f723c39 100644 --- a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts +++ b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts @@ -70,17 +70,18 @@ function getLocatorParams({ shouldRestoreSearchSession: boolean; }): DashboardLocatorParams { const { - componentState: { lastSavedId }, explicitInput: { panels, query, viewMode }, } = container.getState(); + const savedObjectId = container.savedObjectId.value; + return { viewMode, useHash: false, preserveSavedFilters: false, filters: dataService.query.filterManager.getFilters(), query: dataService.query.queryString.formatQuery(query) as Query, - dashboardId: container.getDashboardSavedObjectId(), + dashboardId: savedObjectId, searchSessionId: shouldRestoreSearchSession ? dataService.search.session.getSessionId() : undefined, @@ -93,7 +94,7 @@ function getLocatorParams({ value: 0, } : undefined, - panels: lastSavedId + panels: savedObjectId ? undefined : (convertPanelMapToSavedPanels(panels) as DashboardLocatorParams['panels']), }; diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx index b91e972dffa887..7f51a91379203e 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx @@ -121,7 +121,13 @@ test('DashboardGrid renders expanded panel', async () => { test('DashboardGrid renders focused panel', async () => { const { dashboardApi, component } = await createAndMountDashboardGrid(); - dashboardApi.setFocusedPanelId('2'); + const overlayMock = { + onClose: new Promise((resolve) => { + resolve(); + }), + close: async () => {}, + }; + dashboardApi.openOverlay(overlayMock, { focusedPanelId: '2' }); await new Promise((resolve) => setTimeout(resolve, 1)); component.update(); // Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized. @@ -130,7 +136,7 @@ test('DashboardGrid renders focused panel', async () => { expect(component.find('#mockDashboardGridItem_1').hasClass('blurredPanel')).toBe(true); expect(component.find('#mockDashboardGridItem_2').hasClass('focusedPanel')).toBe(true); - dashboardApi.setFocusedPanelId(undefined); + dashboardApi.clearOverlays(); await new Promise((resolve) => setTimeout(resolve, 1)); component.update(); expect(component.find('GridItem').length).toBe(2); diff --git a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx index 1e4e0822f7ce8e..ac39b3747b1bdc 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx @@ -22,7 +22,10 @@ import { ControlGroupSerializedState, } from '@kbn/controls-plugin/public'; import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common'; -import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import { + useBatchedPublishingSubjects, + useStateFromPublishingSubject, +} from '@kbn/presentation-publishing'; import { DashboardGrid } from '../grid'; import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api'; import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen'; @@ -161,10 +164,7 @@ export const DashboardViewportComponent = () => { const WithFullScreenButton = ({ children }: { children: JSX.Element }) => { const dashboardApi = useDashboardApi(); - const [isFullScreenMode, isEmbeddedExternally] = useBatchedPublishingSubjects( - dashboardApi.fullScreenMode$, - dashboardApi.embeddedExternally$ - ); + const isFullScreenMode = useStateFromPublishingSubject(dashboardApi.fullScreenMode$); return ( <> @@ -173,7 +173,7 @@ const WithFullScreenButton = ({ children }: { children: JSX.Element }) => { dashboardApi.setFullScreenMode(false)} - toggleChrome={!isEmbeddedExternally} + toggleChrome={!dashboardApi.isEmbeddedExternally} /> )} 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 7230ff4fcb619a..444bac28c9e664 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 @@ -80,12 +80,11 @@ const serializeAllPanelState = async ( * Save the current state of this dashboard to a saved object without showing any save modal. */ export async function runQuickSave(this: DashboardContainer) { - const { - explicitInput: currentState, - componentState: { lastSavedId, managed }, - } = this.getState(); + const { explicitInput: currentState } = this.getState(); - if (managed) return; + const lastSavedId = this.savedObjectId.value; + + if (this.managed$.value) return; const { panels: nextPanels, references } = await serializeAllPanelState(this); const dashboardStateToSave: DashboardContainerInput = { ...currentState, panels: nextPanels }; @@ -108,7 +107,7 @@ export async function runQuickSave(this: DashboardContainer) { }); this.savedObjectReferences = saveResult.references ?? []; - this.dispatch.setLastSavedInput(dashboardStateToSave); + this.setLastSavedInput(dashboardStateToSave); this.saveNotification$.next(); return saveResult; @@ -119,11 +118,10 @@ export async function runQuickSave(this: DashboardContainer) { * accounts for scenarios of cloning elastic managed dashboard into user managed dashboards */ export async function runInteractiveSave(this: DashboardContainer, interactionMode: ViewMode) { - const { - explicitInput: currentState, - componentState: { lastSavedId, managed }, - } = this.getState(); + const { explicitInput: currentState } = this.getState(); const dashboardContentManagementService = getDashboardContentManagementService(); + const lastSavedId = this.savedObjectId.value; + const managed = this.managed$.value; return new Promise((resolve, reject) => { if (interactionMode === ViewMode.EDIT && managed) { @@ -242,12 +240,11 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo }, }); - stateFromSaveModal.lastSavedId = saveResult.id; - if (saveResult.id) { batch(() => { this.dispatch.setStateFromSaveModal(stateFromSaveModal); - this.dispatch.setLastSavedInput(dashboardStateToSave); + this.setSavedObjectId(saveResult.id); + this.setLastSavedInput(dashboardStateToSave); }); } 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 1ecc06d90e84af..ea5508dff23da1 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 @@ -97,7 +97,7 @@ test('passes managed state from the saved object into the Dashboard component st }); const dashboard = await createDashboard({}, 0, 'what-an-id'); expect(dashboard).toBeDefined(); - expect(dashboard!.getState().componentState.managed).toBe(true); + expect(dashboard!.managed$.value).toBe(true); }); test('pulls view mode from dashboard backup', async () => { @@ -132,7 +132,7 @@ test('managed dashboards start in view mode', async () => { }); const dashboard = await createDashboard({}, 0, 'what-an-id'); expect(dashboard).toBeDefined(); - expect(dashboard!.getState().componentState.managed).toBe(true); + expect(dashboard!.managed$.value).toBe(true); expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.VIEW); }); 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 2b44abe481b7d5..0264e834bbd072 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 @@ -42,13 +42,14 @@ import { coreServices, dataService, embeddableService } from '../../../services/ import { getDashboardCapabilities } from '../../../utils/get_dashboard_capabilities'; import { runPanelPlacementStrategy } from '../../panel_placement/place_new_panel_strategies'; import { startDiffingDashboardState } from '../../state/diffing/dashboard_diffing_integration'; -import { DashboardPublicState, UnsavedPanelState } from '../../types'; +import { UnsavedPanelState } from '../../types'; import { DashboardContainer } from '../dashboard_container'; import { DashboardCreationOptions } from '../dashboard_container_factory'; import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data_views'; import { startQueryPerformanceTracking } from './performance/query_performance_tracking'; import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration'; import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state'; +import { InitialComponentState } from '../../../dashboard_api/get_dashboard_api'; /** * Builds a new Dashboard from scratch. @@ -100,17 +101,15 @@ export const createDashboard = async ( // -------------------------------------------------------------------------------------- // Build the dashboard container. // -------------------------------------------------------------------------------------- - const initialComponentState: DashboardPublicState = { + const initialComponentState: InitialComponentState = { + anyMigrationRun: savedObjectResult.anyMigrationRun ?? false, + isEmbeddedExternally: creationOptions?.isEmbeddedExternally ?? false, lastSavedInput: omit(savedObjectResult?.dashboardInput, 'controlGroupInput') ?? { ...DEFAULT_DASHBOARD_INPUT, id: input.id, }, - hasRunClientsideMigrations: savedObjectResult.anyMigrationRun, - isEmbeddedExternally: creationOptions?.isEmbeddedExternally, - animatePanelTransforms: false, // set panel transforms to false initially to avoid panels animating on initial render. - hasUnsavedChanges: false, // if there is initial unsaved changes, the initial diff will catch them. - managed: savedObjectResult.managed, lastSavedId: savedObjectId, + managed: savedObjectResult.managed ?? false, }; const dashboardContainer = new DashboardContainer( @@ -473,7 +472,7 @@ export const initializeDashboard = async ({ // Start animating panel transforms 500 ms after dashboard is created. // -------------------------------------------------------------------------------------- untilDashboardReady().then((dashboard) => - setTimeout(() => dashboard.dispatch.setAnimatePanelTransforms(true), 500) + setTimeout(() => dashboard.setAnimatePanelTransforms(true), 500) ); // -------------------------------------------------------------------------------------- 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 160af8a005b1a5..b6043f03b26c0f 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 @@ -82,8 +82,7 @@ export function syncUnifiedSearchState( // if there is no url override time range, check if this dashboard uses time restore, and restore to that. const timeRestoreTimeRange = - this.getState().explicitInput.timeRestore && - this.getState().componentState.lastSavedInput.timeRange; + this.getState().explicitInput.timeRestore && this.lastSavedInput$.value.timeRange; if (timeRestoreTimeRange) { timefilterService.setTime(timeRestoreTimeRange); return timeRestoreTimeRange; @@ -115,8 +114,7 @@ export function syncUnifiedSearchState( // if there is no url override refresh interval, check if this dashboard uses time restore, and restore to that. const timeRestoreRefreshInterval = - this.getState().explicitInput.timeRestore && - this.getState().componentState.lastSavedInput.refreshInterval; + this.getState().explicitInput.timeRestore && this.lastSavedInput$.value.refreshInterval; if (timeRestoreRefreshInterval) { timefilterService.setRefreshInterval(timeRestoreRefreshInterval); return timeRestoreRefreshInterval; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx index 83efeab804ba54..4805c890f5e95e 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx @@ -172,7 +172,13 @@ test('searchSessionId propagates to children', async () => { 0, undefined, undefined, - { lastSavedInput: sampleInput } + { + anyMigrationRun: false, + isEmbeddedExternally: false, + lastSavedInput: sampleInput, + lastSavedId: undefined, + managed: false, + } ); container?.setControlGroupApi(mockControlGroupApi); const embeddable = await container.addNewEmbeddable< 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 161b2f93f12a2b..c6fed0998cfde7 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -11,7 +11,6 @@ import deepEqual from 'fast-deep-equal'; import { omit } from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; -import { batch } from 'react-redux'; import { BehaviorSubject, Subject, @@ -104,12 +103,7 @@ import { getDashboardPanelPlacementSetting } from '../panel_placement/panel_plac import { runPanelPlacementStrategy } from '../panel_placement/place_new_panel_strategies'; import { dashboardContainerReducers } from '../state/dashboard_container_reducers'; import { getDiffingMiddleware } from '../state/diffing/dashboard_diffing_integration'; -import { - DashboardPublicState, - DashboardReduxState, - DashboardStateFromSettingsFlyout, - UnsavedPanelState, -} from '../types'; +import { DashboardReduxState, DashboardStateFromSettingsFlyout, UnsavedPanelState } from '../types'; import { addFromLibrary, addOrUpdateEmbeddable, runInteractiveSave, runQuickSave } from './api'; import { duplicateDashboardPanel } from './api/duplicate_dashboard_panel'; import { @@ -122,6 +116,7 @@ import { dashboardTypeDisplayLowercase, dashboardTypeDisplayName, } from './dashboard_container_factory'; +import { InitialComponentState, getDashboardApi } from '../../dashboard_api/get_dashboard_api'; export interface InheritedChildInput { filters: Filter[]; @@ -163,6 +158,22 @@ export class DashboardContainer public dispatch: DashboardReduxEmbeddableTools['dispatch']; public onStateChange: DashboardReduxEmbeddableTools['onStateChange']; public anyReducerRun: Subject = new Subject(); + public setAnimatePanelTransforms: (animate: boolean) => void; + public setManaged: (managed: boolean) => void; + public setHasUnsavedChanges: (hasUnsavedChanges: boolean) => void; + public openOverlay: (ref: OverlayRef, options?: { focusedPanelId?: string }) => void; + public clearOverlays: () => void; + public highlightPanel: (panelRef: HTMLDivElement) => void; + public setScrollToPanelId: (id: string | undefined) => void; + public setFullScreenMode: (fullScreenMode: boolean) => void; + public setExpandedPanelId: (newId?: string) => void; + public setHighlightPanelId: (highlightPanelId: string | undefined) => void; + public setLastSavedInput: (lastSavedInput: DashboardContainerInput) => void; + public lastSavedInput$: PublishingSubject; + public setSavedObjectId: (id: string | undefined) => void; + public expandPanel: (panelId: string) => void; + public scrollToPanel: (panelRef: HTMLDivElement) => Promise; + public scrollToTop: () => void; public integrationSubscriptions: Subscription = new Subscription(); public publishingSubscription: Subscription = new Subscription(); @@ -182,7 +193,6 @@ export class DashboardContainer public readonly executionContext: KibanaExecutionContext; private domNode?: HTMLElement; - private overlayRef?: OverlayRef; // performance monitoring public lastLoadStartTime?: number; @@ -190,7 +200,6 @@ export class DashboardContainer public creationEndTime?: number; public firstLoad: boolean = true; private hadContentfulRender = false; - private scrollPosition?: number; // setup public untilContainerInitialized: () => Promise; @@ -224,7 +233,7 @@ export class DashboardContainer dashboardCreationStartTime?: number, parent?: Container, creationOptions?: DashboardCreationOptions, - initialComponentState?: DashboardPublicState + initialComponentState?: InitialComponentState ) { const controlGroupApi$ = new BehaviorSubject(undefined); async function untilContainerInitialized(): Promise { @@ -284,7 +293,6 @@ export class DashboardContainer embeddable: this, reducers: dashboardContainerReducers, additionalMiddleware: [diffingMiddleware], - initialComponentState, }); this.onStateChange = reduxTools.onStateChange; this.cleanupStateTools = reduxTools.cleanup; @@ -298,68 +306,55 @@ export class DashboardContainer 'id' ) as BehaviorSubject; - this.savedObjectId = new BehaviorSubject(this.getDashboardSavedObjectId()); - this.expandedPanelId = new BehaviorSubject(this.getExpandedPanelId()); - this.focusedPanelId$ = new BehaviorSubject(this.getState().componentState.focusedPanelId); - this.managed$ = new BehaviorSubject(this.getState().componentState.managed); - this.fullScreenMode$ = new BehaviorSubject(this.getState().componentState.fullScreenMode); - this.hasRunMigrations$ = new BehaviorSubject( - this.getState().componentState.hasRunClientsideMigrations + const dashboardApi = getDashboardApi( + initialComponentState + ? initialComponentState + : { + anyMigrationRun: false, + isEmbeddedExternally: false, + lastSavedInput: initialInput, + lastSavedId: undefined, + managed: false, + }, + (id: string) => this.untilEmbeddableLoaded(id) ); - this.hasUnsavedChanges$ = new BehaviorSubject(this.getState().componentState.hasUnsavedChanges); - this.hasOverlays$ = new BehaviorSubject(this.getState().componentState.hasOverlays); + this.animatePanelTransforms$ = dashboardApi.animatePanelTransforms$; + this.fullScreenMode$ = dashboardApi.fullScreenMode$; + this.hasUnsavedChanges$ = dashboardApi.hasUnsavedChanges$; + this.isEmbeddedExternally = dashboardApi.isEmbeddedExternally; + this.managed$ = dashboardApi.managed$; + this.setAnimatePanelTransforms = dashboardApi.setAnimatePanelTransforms; + this.setFullScreenMode = dashboardApi.setFullScreenMode; + this.setHasUnsavedChanges = dashboardApi.setHasUnsavedChanges; + this.setManaged = dashboardApi.setManaged; + this.expandedPanelId = dashboardApi.expandedPanelId; + this.focusedPanelId$ = dashboardApi.focusedPanelId$; + this.highlightPanelId$ = dashboardApi.highlightPanelId$; + this.highlightPanel = dashboardApi.highlightPanel; + this.setExpandedPanelId = dashboardApi.setExpandedPanelId; + this.setHighlightPanelId = dashboardApi.setHighlightPanelId; + this.scrollToPanelId$ = dashboardApi.scrollToPanelId$; + this.setScrollToPanelId = dashboardApi.setScrollToPanelId; + this.clearOverlays = dashboardApi.clearOverlays; + this.hasOverlays$ = dashboardApi.hasOverlays$; + this.openOverlay = dashboardApi.openOverlay; + this.hasRunMigrations$ = dashboardApi.hasRunMigrations$; + this.setLastSavedInput = dashboardApi.setLastSavedInput; + this.lastSavedInput$ = dashboardApi.lastSavedInput$; + this.savedObjectId = dashboardApi.savedObjectId; + this.setSavedObjectId = dashboardApi.setSavedObjectId; + this.expandPanel = dashboardApi.expandPanel; + this.scrollToPanel = dashboardApi.scrollToPanel; + this.scrollToTop = dashboardApi.scrollToTop; + this.useMargins$ = new BehaviorSubject(this.getState().explicitInput.useMargins); - this.scrollToPanelId$ = new BehaviorSubject(this.getState().componentState.scrollToPanelId); - this.highlightPanelId$ = new BehaviorSubject(this.getState().componentState.highlightPanelId); - this.animatePanelTransforms$ = new BehaviorSubject( - this.getState().componentState.animatePanelTransforms - ); this.panels$ = new BehaviorSubject(this.getState().explicitInput.panels); - this.embeddedExternally$ = new BehaviorSubject( - this.getState().componentState.isEmbeddedExternally - ); this.publishingSubscription.add( this.onStateChange(() => { const state = this.getState(); - if (this.savedObjectId.value !== this.getDashboardSavedObjectId()) { - this.savedObjectId.next(this.getDashboardSavedObjectId()); - } - if (this.expandedPanelId.value !== this.getExpandedPanelId()) { - this.expandedPanelId.next(this.getExpandedPanelId()); - } - if (this.focusedPanelId$.value !== state.componentState.focusedPanelId) { - this.focusedPanelId$.next(state.componentState.focusedPanelId); - } - if (this.managed$.value !== state.componentState.managed) { - this.managed$.next(state.componentState.managed); - } - if (this.fullScreenMode$.value !== state.componentState.fullScreenMode) { - this.fullScreenMode$.next(state.componentState.fullScreenMode); - } - if (this.hasRunMigrations$.value !== state.componentState.hasRunClientsideMigrations) { - this.hasRunMigrations$.next(state.componentState.hasRunClientsideMigrations); - } - if (this.hasUnsavedChanges$.value !== state.componentState.hasUnsavedChanges) { - this.hasUnsavedChanges$.next(state.componentState.hasUnsavedChanges); - } - if (this.hasOverlays$.value !== state.componentState.hasOverlays) { - this.hasOverlays$.next(state.componentState.hasOverlays); - } if (this.useMargins$.value !== state.explicitInput.useMargins) { this.useMargins$.next(state.explicitInput.useMargins); } - if (this.scrollToPanelId$.value !== state.componentState.scrollToPanelId) { - this.scrollToPanelId$.next(state.componentState.scrollToPanelId); - } - if (this.highlightPanelId$.value !== state.componentState.highlightPanelId) { - this.highlightPanelId$.next(state.componentState.highlightPanelId); - } - if (this.animatePanelTransforms$.value !== state.componentState.animatePanelTransforms) { - this.animatePanelTransforms$.next(state.componentState.animatePanelTransforms); - } - if (this.embeddedExternally$.value !== state.componentState.isEmbeddedExternally) { - this.embeddedExternally$.next(state.componentState.isEmbeddedExternally); - } if (this.panels$.value !== state.explicitInput.panels) { this.panels$.next(state.explicitInput.panels); } @@ -438,7 +433,7 @@ export class DashboardContainer public getAppContext() { const embeddableAppContext = this.creationOptions?.getEmbeddableAppContext?.( - this.getDashboardSavedObjectId() + this.savedObjectId.value ); return { ...embeddableAppContext, @@ -446,10 +441,6 @@ export class DashboardContainer }; } - public getDashboardSavedObjectId() { - return this.getState().componentState.lastSavedId; - } - protected createNewPanelState< TEmbeddableInput extends EmbeddableInput, TEmbeddable extends IEmbeddable @@ -493,7 +484,7 @@ export class DashboardContainer public updateInput(changes: Partial): void { // block the Dashboard from entering edit mode if this Dashboard is managed. if ( - (this.getState().componentState.managed || !this.showWriteControls) && + (this.managed$.value || !this.showWriteControls) && changes.viewMode?.toLowerCase() === ViewMode.EDIT?.toLowerCase() ) { const { viewMode, ...rest } = changes; @@ -570,7 +561,7 @@ export class DashboardContainer duplicateDashboardPanel.bind(this)(id); } - public canRemovePanels = () => !this.getExpandedPanelId(); + public canRemovePanels = () => this.expandedPanelId.value === undefined; public getTypeDisplayName = () => dashboardTypeDisplayName; public getTypeDisplayNameLowerCase = () => dashboardTypeDisplayLowercase; @@ -578,17 +569,17 @@ export class DashboardContainer public savedObjectId: BehaviorSubject; public expandedPanelId: BehaviorSubject; public focusedPanelId$: BehaviorSubject; - public managed$: BehaviorSubject; - public fullScreenMode$: BehaviorSubject; - public hasRunMigrations$: BehaviorSubject; - public hasUnsavedChanges$: BehaviorSubject; - public hasOverlays$: BehaviorSubject; + public managed$: BehaviorSubject; + public fullScreenMode$: BehaviorSubject; + public hasRunMigrations$: BehaviorSubject; + public hasUnsavedChanges$: BehaviorSubject; + public hasOverlays$: BehaviorSubject; public useMargins$: BehaviorSubject; public scrollToPanelId$: BehaviorSubject; public highlightPanelId$: BehaviorSubject; - public animatePanelTransforms$: BehaviorSubject; + public animatePanelTransforms$: BehaviorSubject; public panels$: BehaviorSubject; - public embeddedExternally$: BehaviorSubject; + public isEmbeddedExternally: boolean; public uuid$: BehaviorSubject; public async replacePanel(idToRemove: string, { panelType, initialState }: PanelPackage) { @@ -598,7 +589,7 @@ export class DashboardContainer panelType, true ); - if (this.getExpandedPanelId() !== undefined) { + if (this.expandedPanelId.value !== undefined) { this.setExpandedPanelId(newId); } this.setHighlightPanelId(newId); @@ -720,21 +711,6 @@ export class DashboardContainer return panel; }; - public expandPanel = (panelId: string) => { - const isPanelExpanded = Boolean(this.getExpandedPanelId()); - - if (isPanelExpanded) { - this.setExpandedPanelId(undefined); - this.setScrollToPanelId(panelId); - return; - } - - this.setExpandedPanelId(panelId); - if (window.scrollY > 0) { - this.scrollPosition = window.scrollY; - } - }; - public addOrUpdateEmbeddable = addOrUpdateEmbeddable; public forceRefresh(refreshControlGroup: boolean = true) { @@ -746,14 +722,13 @@ export class DashboardContainer } public async asyncResetToLastSavedState() { - this.dispatch.resetToLastSavedInput({}); + this.dispatch.resetToLastSavedInput(this.lastSavedInput$.value); const { explicitInput: { timeRange, refreshInterval }, - componentState: { - lastSavedInput: { timeRestore: lastSavedTimeRestore }, - }, } = this.getState(); + const { timeRestore: lastSavedTimeRestore } = this.lastSavedInput$.value; + if (this.controlGroupApi$.value) { await this.controlGroupApi$.value.asyncResetUnsavedChanges(); } @@ -802,15 +777,11 @@ export class DashboardContainer this.searchSessionId = searchSessionId; this.searchSessionId$.next(searchSessionId); - batch(() => { - this.dispatch.setLastSavedInput( - omit(loadDashboardReturn?.dashboardInput, 'controlGroupInput') - ); - this.dispatch.setManaged(loadDashboardReturn?.managed); - this.dispatch.setAnimatePanelTransforms(false); // prevents panels from animating on navigate. - this.dispatch.setLastSavedId(newSavedObjectId); - this.setExpandedPanelId(undefined); - }); + this.setAnimatePanelTransforms(false); // prevents panels from animating on navigate. + this.setManaged(loadDashboardReturn?.managed ?? false); + this.setExpandedPanelId(undefined); + this.setLastSavedInput(omit(loadDashboardReturn?.dashboardInput, 'controlGroupInput')); + this.setSavedObjectId(newSavedObjectId); this.firstLoad = true; this.updateInput(newInput); dashboardContainerReady$.next(this); @@ -824,10 +795,6 @@ export class DashboardContainer (this.dataViews as BehaviorSubject).next(newDataViews); }; - public getExpandedPanelId = () => { - return this.getState().componentState.expandedPanelId; - }; - public getPanelsState = () => { return this.getState().explicitInput.panels; }; @@ -837,7 +804,6 @@ export class DashboardContainer return { description: state.explicitInput.description, hidePanelTitles: state.explicitInput.hidePanelTitles, - lastSavedId: state.componentState.lastSavedId, syncColors: state.explicitInput.syncColors, syncCursor: state.explicitInput.syncCursor, syncTooltips: state.explicitInput.syncTooltips, @@ -852,18 +818,14 @@ export class DashboardContainer this.dispatch.setStateFromSettingsFlyout(settings); }; - public setExpandedPanelId = (newId?: string) => { - this.dispatch.setExpandedPanelId(newId); - }; - public setViewMode = (viewMode: ViewMode) => { + // block the Dashboard from entering edit mode if this Dashboard is managed. + if (this.managed$.value && viewMode?.toLowerCase() === ViewMode.EDIT) { + return; + } this.dispatch.setViewMode(viewMode); }; - public setFullScreenMode = (fullScreenMode: boolean) => { - this.dispatch.setFullScreenMode(fullScreenMode); - }; - public setQuery = (query?: Query | undefined) => this.updateInput({ query }); public setFilters = (filters?: Filter[] | undefined) => this.updateInput({ filters }); @@ -872,21 +834,6 @@ export class DashboardContainer this.updateInput({ tags }); }; - public openOverlay = (ref: OverlayRef, options?: { focusedPanelId?: string }) => { - this.clearOverlays(); - this.dispatch.setHasOverlays(true); - this.overlayRef = ref; - if (options?.focusedPanelId) { - this.setFocusedPanelId(options?.focusedPanelId); - } - }; - - public clearOverlays = () => { - this.dispatch.setHasOverlays(false); - this.dispatch.setFocusedPanelId(undefined); - this.overlayRef?.close(); - }; - public getPanelCount = () => { return Object.keys(this.getInput().panels).length; }; @@ -909,59 +856,6 @@ export class DashboardContainer return titles; } - public setScrollToPanelId = (id: string | undefined) => { - this.dispatch.setScrollToPanelId(id); - }; - - public scrollToPanel = async (panelRef: HTMLDivElement) => { - const id = this.getState().componentState.scrollToPanelId; - if (!id) return; - - this.untilEmbeddableLoaded(id).then(() => { - this.setScrollToPanelId(undefined); - if (this.scrollPosition) { - panelRef.ontransitionend = () => { - // Scroll to the last scroll position after the transition ends to ensure the panel is back in the right position before scrolling - // This is necessary because when an expanded panel collapses, it takes some time for the panel to return to its original position - window.scrollTo({ top: this.scrollPosition }); - this.scrollPosition = undefined; - panelRef.ontransitionend = null; - }; - return; - } - - panelRef.scrollIntoView({ block: 'center' }); - }); - }; - - public scrollToTop = () => { - window.scroll(0, 0); - }; - - public setHighlightPanelId = (id: string | undefined) => { - this.dispatch.setHighlightPanelId(id); - }; - - public highlightPanel = (panelRef: HTMLDivElement) => { - const id = this.getState().componentState.highlightPanelId; - - if (id && panelRef) { - this.untilEmbeddableLoaded(id).then(() => { - panelRef.classList.add('dshDashboardGrid__item--highlighted'); - // Removes the class after the highlight animation finishes - setTimeout(() => { - panelRef.classList.remove('dshDashboardGrid__item--highlighted'); - }, 5000); - }); - } - this.setHighlightPanelId(undefined); - }; - - public setFocusedPanelId = (id: string | undefined) => { - this.dispatch.setFocusedPanelId(id); - this.setScrollToPanelId(id); - }; - public setPanels = (panels: DashboardPanelMap) => { this.dispatch.setPanels(panels); }; diff --git a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts index 74b9ea715d85ac..0bb33a05c36cea 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts @@ -8,11 +8,9 @@ */ import { PayloadAction } from '@reduxjs/toolkit'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; import { DashboardReduxState, - DashboardPublicState, DashboardStateFromSaveModal, DashboardStateFromSettingsFlyout, } from '../types'; @@ -36,8 +34,6 @@ export const dashboardContainerReducers = { state: DashboardReduxState, action: PayloadAction ) => { - state.componentState.lastSavedId = action.payload.lastSavedId; - state.explicitInput.tags = action.payload.tags; state.explicitInput.title = action.payload.title; state.explicitInput.description = action.payload.description; @@ -51,16 +47,10 @@ export const dashboardContainerReducers = { } }, - setLastSavedId: (state: DashboardReduxState, action: PayloadAction) => { - state.componentState.lastSavedId = action.payload; - }, - setStateFromSettingsFlyout: ( state: DashboardReduxState, action: PayloadAction ) => { - state.componentState.lastSavedId = action.payload.lastSavedId; - state.explicitInput.tags = action.payload.tags; state.explicitInput.title = action.payload.title; state.explicitInput.description = action.payload.description; @@ -84,11 +74,6 @@ export const dashboardContainerReducers = { state: DashboardReduxState, action: PayloadAction ) => { - // Managed Dashboards cannot be put into edit mode. - if (state.componentState.managed) { - state.explicitInput.viewMode = ViewMode.VIEW; - return; - } state.explicitInput.viewMode = action.payload; }, @@ -103,34 +88,6 @@ export const dashboardContainerReducers = { state.explicitInput.title = action.payload; }, - setManaged: ( - state: DashboardReduxState, - action: PayloadAction - ) => { - state.componentState.managed = action.payload; - }, - - // ------------------------------------------------------------------------------ - // Unsaved Changes Reducers - // ------------------------------------------------------------------------------ - setHasUnsavedChanges: ( - state: DashboardReduxState, - action: PayloadAction - ) => { - state.componentState.hasUnsavedChanges = action.payload; - }, - - setLastSavedInput: ( - state: DashboardReduxState, - action: PayloadAction - ) => { - state.componentState.lastSavedInput = action.payload; - - // if we set the last saved input, it means we have saved this Dashboard - therefore clientside migrations have - // been serialized into the SO. - state.componentState.hasRunClientsideMigrations = false; - }, - /** * Resets the dashboard to the last saved input, excluding: * 1) The time range, unless `timeRestore` is `true` - if we include the time range on reset even when @@ -138,9 +95,12 @@ export const dashboardContainerReducers = { * 2) The view mode, since resetting should never impact this - sometimes the Dashboard saved objects * have this saved in and we don't want resetting to cause unexpected view mode changes. */ - resetToLastSavedInput: (state: DashboardReduxState) => { + resetToLastSavedInput: ( + state: DashboardReduxState, + action: PayloadAction + ) => { state.explicitInput = { - ...state.componentState.lastSavedInput, + ...action.payload, ...(!state.explicitInput.timeRestore && { timeRange: state.explicitInput.timeRange }), viewMode: state.explicitInput.viewMode, }; @@ -175,13 +135,6 @@ export const dashboardContainerReducers = { state.explicitInput.query = action.payload; }, - setSavedQueryId: ( - state: DashboardReduxState, - action: PayloadAction - ) => { - state.componentState.savedQueryId = action.payload; - }, - setTimeRestore: ( state: DashboardReduxState, action: PayloadAction @@ -209,38 +162,4 @@ export const dashboardContainerReducers = { ) => { state.explicitInput.timeslice = action.payload; }, - - setExpandedPanelId: (state: DashboardReduxState, action: PayloadAction) => { - state.componentState.expandedPanelId = action.payload; - }, - - setFullScreenMode: (state: DashboardReduxState, action: PayloadAction) => { - state.componentState.fullScreenMode = action.payload; - }, - - // ------------------------------------------------------------------------------ - // Component state reducers - // ------------------------------------------------------------------------------ - - setHasOverlays: (state: DashboardReduxState, action: PayloadAction) => { - state.componentState.hasOverlays = action.payload; - }, - - setScrollToPanelId: (state: DashboardReduxState, action: PayloadAction) => { - state.componentState.scrollToPanelId = action.payload; - }, - - setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction) => { - state.componentState.highlightPanelId = action.payload; - }, - setFocusedPanelId: (state: DashboardReduxState, action: PayloadAction) => { - state.componentState.focusedPanelId = 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 1bcba571dc24fa..8c014d0ec92c64 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 @@ -26,12 +26,7 @@ import { isKeyEqualAsync, unsavedChangesDiffingFunctions } from './dashboard_dif * and the last saved input, so we can safely ignore any output reducers, and most componentState reducers. * This is only for performance reasons, because the diffing function itself can be quite heavy. */ -export const reducersToIgnore: Array = [ - 'setTimeslice', - 'setFullScreenMode', - 'setExpandedPanelId', - 'setHasUnsavedChanges', -]; +export const reducersToIgnore: Array = ['setTimeslice']; /** * Some keys will often have deviated from their last saved state, but should not persist over reloads @@ -89,15 +84,14 @@ export function startDiffingDashboardState( * Create an observable stream that checks for unsaved changes in the Dashboard state * and the state of all of its legacy embeddable children. */ - const dashboardUnsavedChanges = this.anyReducerRun.pipe( - startWith(null), + const dashboardUnsavedChanges = combineLatest([ + this.anyReducerRun.pipe(startWith(null)), + this.lastSavedInput$, + ]).pipe( debounceTime(CHANGE_CHECK_DEBOUNCE), - switchMap(() => { + switchMap(([, lastSavedInput]) => { return (async () => { - const { - explicitInput: currentInput, - componentState: { lastSavedInput }, - } = this.getState(); + const { explicitInput: currentInput } = this.getState(); const unsavedChanges = await getDashboardUnsavedChanges.bind(this)( lastSavedInput, currentInput @@ -126,8 +120,8 @@ export function startDiffingDashboardState( Object.keys(omit(dashboardChanges, keysNotConsideredUnsavedChanges)).length > 0 || unsavedPanelState !== undefined || controlGroupChanges !== undefined; - if (hasUnsavedChanges !== this.getState().componentState.hasUnsavedChanges) { - this.dispatch.setHasUnsavedChanges(hasUnsavedChanges); + if (hasUnsavedChanges !== this.hasUnsavedChanges$.value) { + this.setHasUnsavedChanges(hasUnsavedChanges); } // backup unsaved changes if configured to do so @@ -193,7 +187,7 @@ function backupUnsavedChanges( const dashboardStateToBackup = omit(dashboardChanges, keysToOmitFromSessionStorage); getDashboardBackupService().setState( - this.getDashboardSavedObjectId(), + this.savedObjectId.value, { ...dashboardStateToBackup, panels: dashboardChanges.panels, diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index 4119b78f055274..f0b6ea2621abd7 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -21,8 +21,7 @@ export interface UnsavedPanelState { export type DashboardReduxState = ReduxEmbeddableState< DashboardContainerInput, - DashboardContainerOutput, - DashboardPublicState + DashboardContainerOutput >; export type DashboardRedirect = (props: RedirectToProps) => void; @@ -33,28 +32,10 @@ export type RedirectToProps = export type DashboardStateFromSaveModal = Pick< DashboardContainerInput, 'title' | 'description' | 'tags' | 'timeRestore' | 'timeRange' | 'refreshInterval' -> & - Pick; +>; export type DashboardStateFromSettingsFlyout = DashboardStateFromSaveModal & DashboardOptions; -export interface DashboardPublicState { - lastSavedInput: DashboardContainerInput; - hasRunClientsideMigrations?: boolean; - animatePanelTransforms?: boolean; - isEmbeddedExternally?: boolean; - hasUnsavedChanges?: boolean; - hasOverlays?: boolean; - expandedPanelId?: string; - fullScreenMode?: boolean; - savedQueryId?: string; - lastSavedId?: string; - managed?: boolean; - scrollToPanelId?: string; - highlightPanelId?: string; - focusedPanelId?: string; -} - export type DashboardLoadType = | 'sessionFirstLoad' | 'dashboardFirstLoad' @@ -89,10 +70,7 @@ export interface DashboardSaveOptions { } export type DashboardLocatorParams = Partial< - Omit< - DashboardContainerInput, - 'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally' - > + Omit > & { /** * If given, the dashboard saved object with this id will be loaded. If not given, diff --git a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx index 8cf7f36918ddc2..0c3b4b583e8bf2 100644 --- a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx +++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.test.tsx @@ -8,6 +8,7 @@ */ import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import { render } from '@testing-library/react'; import { buildMockDashboard } from '../mocks'; import { InternalDashboardTopNav } from './internal_dashboard_top_nav'; @@ -50,9 +51,12 @@ describe('Internal dashboard top nav', () => { it('should render the managed badge when the dashboard is managed', async () => { const container = buildMockDashboard(); - container.dispatch.setManaged(true); + const dashboardApi = { + ...container, + managed$: new BehaviorSubject(true), + } as unknown as DashboardApi; const component = render( - + ); diff --git a/src/plugins/dashboard/public/mocks.tsx b/src/plugins/dashboard/public/mocks.tsx index cef77634a996ee..17081084d5dec4 100644 --- a/src/plugins/dashboard/public/mocks.tsx +++ b/src/plugins/dashboard/public/mocks.tsx @@ -93,7 +93,13 @@ export function buildMockDashboard({ undefined, undefined, undefined, - { lastSavedInput: initialInput, lastSavedId: savedObjectId } + { + anyMigrationRun: false, + isEmbeddedExternally: false, + lastSavedInput: initialInput, + lastSavedId: savedObjectId, + managed: false, + } ); dashboardContainer?.setControlGroupApi(mockControlGroupApi); return dashboardContainer; @@ -113,7 +119,6 @@ export function getSampleDashboardInput( id: '123', tags: [], filters: [], - isEmbeddedExternally: false, title: 'My Dashboard', query: { language: 'kuery', diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 0f4059edfb0bfd..57125918ef3fc3 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -79,6 +79,7 @@ "@kbn/managed-content-badge", "@kbn/content-management-favorites-public", "@kbn/core-custom-branding-browser-mocks", + "@kbn/core-mount-utils-browser", ], "exclude": ["target/**/*"] }