diff --git a/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts index 3db6129c7c6f9..3305f76fac1f2 100644 --- a/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -8,40 +8,53 @@ */ import { BehaviorSubject } from 'rxjs'; +import { omit } from 'lodash'; import type { DashboardContainerInput } from '../../common'; import { initializeTrackPanel } from './track_panel'; import { initializeTrackOverlay } from './track_overlay'; import { initializeUnsavedChanges } from './unsaved_changes'; +import { initializePanelsManager } from './panels_manager'; +import { LoadDashboardReturn } from '../services/dashboard_content_management_service/types'; +import { DEFAULT_DASHBOARD_INPUT } from '../dashboard_constants'; -export interface InitialComponentState { - anyMigrationRun: boolean; - isEmbeddedExternally: boolean; - lastSavedInput: DashboardContainerInput; - lastSavedId: string | undefined; - managed: boolean; -} - -export function getDashboardApi( - initialComponentState: InitialComponentState, - untilEmbeddableLoaded: (id: string) => Promise -) { +export function getDashboardApi({ + isEmbeddedExternally, + savedObjectId, + savedObjectResult, + initialInput, + untilEmbeddableLoaded, +}: { + isEmbeddedExternally?: boolean; + savedObjectId?: string; + savedObjectResult?: LoadDashboardReturn; + initialInput: DashboardContainerInput; + 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 managed$ = new BehaviorSubject(savedObjectResult?.managed ?? false); + const savedObjectId$ = new BehaviorSubject(savedObjectId); const trackPanel = initializeTrackPanel(untilEmbeddableLoaded); return { ...trackPanel, + ...initializePanelsManager( + initialInput.panels, + savedObjectResult?.references ?? [], + trackPanel + ), ...initializeTrackOverlay(trackPanel.setFocusedPanelId), ...initializeUnsavedChanges( - initialComponentState.anyMigrationRun, - initialComponentState.lastSavedInput + savedObjectResult?.anyMigrationRun ?? false, + omit(savedObjectResult?.dashboardInput, 'controlGroupInput') ?? { + ...DEFAULT_DASHBOARD_INPUT, + id: initialInput.id, + } ), animatePanelTransforms$, fullScreenMode$, - isEmbeddedExternally: initialComponentState.isEmbeddedExternally, + isEmbeddedExternally: isEmbeddedExternally ?? false, managed$, savedObjectId: savedObjectId$, setAnimatePanelTransforms: (animate: boolean) => animatePanelTransforms$.next(animate), diff --git a/src/plugins/dashboard/public/dashboard_api/panels_manager.ts b/src/plugins/dashboard/public/dashboard_api/panels_manager.ts new file mode 100644 index 0000000000000..1355eb411b2fc --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_api/panels_manager.ts @@ -0,0 +1,178 @@ +/* + * 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, merge } from 'rxjs'; +import { v4 } from 'uuid'; +import type { Reference } from '@kbn/content-management-utils'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { PanelPackage } from '@kbn/presentation-containers'; +import { PanelNotFoundError } from '@kbn/embeddable-plugin/public'; +import { apiPublishesUnsavedChanges } from '@kbn/presentation-publishing'; +import { coreServices, usageCollectionService } from '../services/kibana_services'; +import { DashboardPanelMap, DashboardPanelState } from '../../common'; +import { getReferencesForPanelId } from '../../common/dashboard_container/persistable_state/dashboard_container_references'; +import type { initializeTrackPanel } from './track_panel'; +import { getPanelAddedSuccessString } from '../dashboard_app/_dashboard_app_strings'; +import { runPanelPlacementStrategy } from '../dashboard_container/panel_placement/place_new_panel_strategies'; +import { + DASHBOARD_UI_METRIC_ID, + DEFAULT_PANEL_HEIGHT, + DEFAULT_PANEL_WIDTH, + PanelPlacementStrategy, +} from '../dashboard_constants'; +import { getDashboardPanelPlacementSetting } from '../dashboard_container/panel_placement/panel_placement_registry'; +import { UnsavedPanelState } from '../dashboard_container/types'; + +export function initializePanelsManager( + initialPanels: DashboardPanelMap, + initialReferences: Reference[], + trackPanel: ReturnType +) { + const children$ = new BehaviorSubject<{ + [key: string]: unknown; + }>({}); + const panels$ = new BehaviorSubject(initialPanels); + let references: Reference[] = initialReferences; + let restoredRuntimeState: UnsavedPanelState = {}; + + function setRuntimeStateForChild(childId: string, state: object) { + restoredRuntimeState[childId] = state; + } + + async function untilReactEmbeddableLoaded(id: string): Promise { + if (!panels$.value[id]) { + throw new PanelNotFoundError(); + } + + if (children$.value[id]) { + return children$.value[id] as ApiType; + } + + return new Promise((resolve, reject) => { + const subscription = merge(children$, panels$).subscribe(() => { + if (children$.value[id]) { + subscription.unsubscribe(); + resolve(children$.value[id] as ApiType); + } + + // If we hit this, the panel was removed before the embeddable finished loading. + if (panels$.value[id] === undefined) { + subscription.unsubscribe(); + resolve(undefined); + } + }); + }); + } + + return { + addNewPanel: async ( + panelPackage: PanelPackage, + displaySuccessMessage?: boolean + ) => { + usageCollectionService?.reportUiCounter( + DASHBOARD_UI_METRIC_ID, + METRIC_TYPE.CLICK, + panelPackage.panelType + ); + + const newId = v4(); + + const getCustomPlacementSettingFunc = getDashboardPanelPlacementSetting( + panelPackage.panelType + ); + + const customPlacementSettings = getCustomPlacementSettingFunc + ? await getCustomPlacementSettingFunc(panelPackage.initialState) + : undefined; + + const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy( + customPlacementSettings?.strategy ?? PanelPlacementStrategy.findTopLeftMostOpenSpace, + { + currentPanels: panels$.value, + height: customPlacementSettings?.height ?? DEFAULT_PANEL_HEIGHT, + width: customPlacementSettings?.width ?? DEFAULT_PANEL_WIDTH, + } + ); + const newPanel: DashboardPanelState = { + type: panelPackage.panelType, + gridData: { + ...newPanelPlacement, + i: newId, + }, + explicitInput: { + id: newId, + }, + }; + if (panelPackage.initialState) { + setRuntimeStateForChild(newId, panelPackage.initialState); + } + panels$.next({ ...otherPanels, [newId]: newPanel }); + if (displaySuccessMessage) { + coreServices.notifications.toasts.addSuccess({ + title: getPanelAddedSuccessString(newPanel.explicitInput.title), + 'data-test-subj': 'addEmbeddableToDashboardSuccess', + }); + trackPanel.setScrollToPanelId(newId); + trackPanel.setHighlightPanelId(newId); + } + return await untilReactEmbeddableLoaded(newId); + }, + canRemovePanels: () => trackPanel.expandedPanelId.value === undefined, + children$, + getSerializedStateForChild: (childId: string) => { + const rawState = panels$.value[childId]?.explicitInput ?? { id: childId }; + const { id, ...serializedState } = rawState; + return Object.keys(serializedState).length === 0 + ? undefined + : { + rawState, + references: getReferencesForPanelId(childId, references), + }; + }, + getRuntimeStateForChild: (childId: string) => { + return restoredRuntimeState?.[childId]; + }, + panels$, + references, + removePanel: (id: string) => { + const panels = { ...panels$.value }; + if (panels[id]) { + delete panels[id]; + panels$.next(panels); + } + const children = { ...children$.value }; + if (children[id]) { + delete children[id]; + children$.next(children); + } + }, + resetAllReactEmbeddables: () => { + restoredRuntimeState = {}; + let resetChangedPanelCount = false; + const currentChildren = children$.value; + for (const panelId of Object.keys(currentChildren)) { + if (panels$.value[panelId]) { + const child = currentChildren[panelId]; + if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges(); + } else { + // if reset resulted in panel removal, we need to update the list of children + delete currentChildren[panelId]; + resetChangedPanelCount = true; + } + } + if (resetChangedPanelCount) children$.next(currentChildren); + }, + setChildren: (children: { [key: string]: unknown; }) => children$.next(children), + setPanels: (panels: DashboardPanelMap) => { + panels$.next(panels); + }, + setReferences: (nextReferences: Reference[]) => (references = nextReferences), + setRuntimeStateForChild, + }; +} 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 444bac28c9e66..b77e0e2f7db8b 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 @@ -106,7 +106,7 @@ export async function runQuickSave(this: DashboardContainer) { lastSavedId, }); - this.savedObjectReferences = saveResult.references ?? []; + this.setReferences(saveResult.references ?? []); this.setLastSavedInput(dashboardStateToSave); this.saveNotification$.next(); @@ -248,7 +248,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo }); } - this.savedObjectReferences = saveResult.references ?? []; + this.setReferences(saveResult.references ?? []); this.saveNotification$.next(); resolve(saveResult); 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 0264e834bbd07..acba535fc3c49 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 @@ -49,7 +49,6 @@ import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data 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. @@ -101,17 +100,6 @@ export const createDashboard = async ( // -------------------------------------------------------------------------------------- // Build the dashboard container. // -------------------------------------------------------------------------------------- - const initialComponentState: InitialComponentState = { - anyMigrationRun: savedObjectResult.anyMigrationRun ?? false, - isEmbeddedExternally: creationOptions?.isEmbeddedExternally ?? false, - lastSavedInput: omit(savedObjectResult?.dashboardInput, 'controlGroupInput') ?? { - ...DEFAULT_DASHBOARD_INPUT, - id: input.id, - }, - lastSavedId: savedObjectId, - managed: savedObjectResult.managed ?? false, - }; - const dashboardContainer = new DashboardContainer( input, reduxEmbeddablePackage, @@ -119,7 +107,11 @@ export const createDashboard = async ( dashboardCreationStartTime, undefined, creationOptions, - initialComponentState + { + isEmbeddedExternally: creationOptions?.isEmbeddedExternally ?? false, + savedObjectId, + savedObjectResult, + } ); // -------------------------------------------------------------------------------------- @@ -272,7 +264,6 @@ export const initializeDashboard = async ({ // Track references // -------------------------------------------------------------------------------------- untilDashboardReady().then((dashboard) => { - dashboard.savedObjectReferences = loadDashboardReturn?.references; dashboard.controlGroupInput = loadDashboardReturn?.dashboardInput?.controlGroupInput; }); 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 c6fed0998cfde..7c957bcf8c1ee 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -21,7 +21,6 @@ import { skipWhile, switchMap, } from 'rxjs'; -import { v4 } from 'uuid'; import { METRIC_TYPE } from '@kbn/analytics'; import type { Reference } from '@kbn/content-management-utils'; @@ -48,6 +47,7 @@ import { HasSaveNotification, HasSerializedChildState, PanelPackage, + PresentationContainer, TrackContentfulRender, TracksQueryPerformance, combineCompatibleChildrenApis, @@ -59,7 +59,6 @@ import { PublishesViewMode, apiPublishesDataLoading, apiPublishesPanelTitle, - apiPublishesUnsavedChanges, getPanelTitle, type PublishingSubject, } from '@kbn/presentation-publishing'; @@ -75,19 +74,10 @@ import { DashboardPanelMap, DashboardPanelState, } from '../../../common'; -import { - getReferencesForControls, - getReferencesForPanelId, -} from '../../../common/dashboard_container/persistable_state/dashboard_container_references'; +import { getReferencesForControls } from '../../../common/dashboard_container/persistable_state/dashboard_container_references'; import { DashboardContext } from '../../dashboard_api/use_dashboard_api'; import { getPanelAddedSuccessString } from '../../dashboard_app/_dashboard_app_strings'; -import { - DASHBOARD_APP_ID, - DASHBOARD_UI_METRIC_ID, - DEFAULT_PANEL_HEIGHT, - DEFAULT_PANEL_WIDTH, - PanelPlacementStrategy, -} from '../../dashboard_constants'; +import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants'; import { PANELS_CONTROL_GROUP_KEY } from '../../services/dashboard_backup_service'; import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; import { @@ -99,11 +89,9 @@ import { import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities'; import { DashboardViewport } from '../component/viewport/dashboard_viewport'; import { placePanel } from '../panel_placement'; -import { getDashboardPanelPlacementSetting } from '../panel_placement/panel_placement_registry'; -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 { DashboardReduxState, DashboardStateFromSettingsFlyout, UnsavedPanelState } from '../types'; +import { DashboardReduxState, DashboardStateFromSettingsFlyout } from '../types'; import { addFromLibrary, addOrUpdateEmbeddable, runInteractiveSave, runQuickSave } from './api'; import { duplicateDashboardPanel } from './api/duplicate_dashboard_panel'; import { @@ -116,7 +104,8 @@ import { dashboardTypeDisplayLowercase, dashboardTypeDisplayName, } from './dashboard_container_factory'; -import { InitialComponentState, getDashboardApi } from '../../dashboard_api/get_dashboard_api'; +import { getDashboardApi } from '../../dashboard_api/get_dashboard_api'; +import { LoadDashboardReturn } from '../../services/dashboard_content_management_service/types'; export interface InheritedChildInput { filters: Filter[]; @@ -174,6 +163,13 @@ export class DashboardContainer public expandPanel: (panelId: string) => void; public scrollToPanel: (panelRef: HTMLDivElement) => Promise; public scrollToTop: () => void; + public setReferences: (nextReferences: Reference[]) => void; + public getSerializedStateForChild: HasSerializedChildState['getSerializedStateForChild']; + public getRuntimeStateForChild: HasRuntimeChildState['getRuntimeStateForChild']; + public setRuntimeStateForChild: (childId: string, state: object) => void; + public canRemovePanels: PresentationContainer['canRemovePanels']; + public removePanel: PresentationContainer['removePanel']; + private dashboardApi: ReturnType; public integrationSubscriptions: Subscription = new Subscription(); public publishingSubscription: Subscription = new Subscription(); @@ -223,7 +219,7 @@ export class DashboardContainer | ((type: string, eventNames: string | string[], count?: number | undefined) => void) | undefined; // new embeddable framework - public savedObjectReferences: Reference[] = []; + public savedObjectReferences: Reference[]; public controlGroupInput: DashboardAttributes['controlGroupInput'] | undefined; constructor( @@ -233,7 +229,11 @@ export class DashboardContainer dashboardCreationStartTime?: number, parent?: Container, creationOptions?: DashboardCreationOptions, - initialComponentState?: InitialComponentState + creationState?: { + isEmbeddedExternally: boolean; + savedObjectId?: string; + savedObjectResult?: LoadDashboardReturn; + } ) { const controlGroupApi$ = new BehaviorSubject(undefined); async function untilContainerInitialized(): Promise { @@ -306,18 +306,11 @@ export class DashboardContainer 'id' ) as BehaviorSubject; - const dashboardApi = getDashboardApi( - initialComponentState - ? initialComponentState - : { - anyMigrationRun: false, - isEmbeddedExternally: false, - lastSavedInput: initialInput, - lastSavedId: undefined, - managed: false, - }, - (id: string) => this.untilEmbeddableLoaded(id) - ); + const dashboardApi = getDashboardApi({ + ...(creationState ?? {}), + initialInput: this.getState().explicitInput, + untilEmbeddableLoaded: (id: string) => this.untilEmbeddableLoaded(id), + }); this.animatePanelTransforms$ = dashboardApi.animatePanelTransforms$; this.fullScreenMode$ = dashboardApi.fullScreenMode$; this.hasUnsavedChanges$ = dashboardApi.hasUnsavedChanges$; @@ -346,17 +339,37 @@ export class DashboardContainer this.expandPanel = dashboardApi.expandPanel; this.scrollToPanel = dashboardApi.scrollToPanel; this.scrollToTop = dashboardApi.scrollToTop; + this.savedObjectReferences = dashboardApi.references; + this.setReferences = dashboardApi.setReferences; + this.getSerializedStateForChild = dashboardApi.getSerializedStateForChild; + this.getRuntimeStateForChild = dashboardApi.getRuntimeStateForChild; + this.panels$ = dashboardApi.panels$; + this.setRuntimeStateForChild = dashboardApi.setRuntimeStateForChild; + this.canRemovePanels = dashboardApi.canRemovePanels; + this.removePanel = dashboardApi.removePanel; + this.children$ = dashboardApi.children$; + this.getChildren = () => { + return { ...this.children$.value }; + }; + this.setChildren = dashboardApi.setChildren; + this.dashboardApi = dashboardApi; this.useMargins$ = new BehaviorSubject(this.getState().explicitInput.useMargins); - this.panels$ = new BehaviorSubject(this.getState().explicitInput.panels); this.publishingSubscription.add( - this.onStateChange(() => { - const state = this.getState(); - if (this.useMargins$.value !== state.explicitInput.useMargins) { - this.useMargins$.next(state.explicitInput.useMargins); + this.getInput$().subscribe((input) => { + if (this.useMargins$.value !== input.useMargins) { + this.useMargins$.next(input.useMargins); } - if (this.panels$.value !== state.explicitInput.panels) { - this.panels$.next(state.explicitInput.panels); + if (this.panels$.value !== input.panels) { + dashboardApi.setPanels(input.panels); + } + }) + ); + + this.publishingSubscription.add( + this.panels$.subscribe((panels) => { + if (panels !== this.getInput().panels) { + this.updateInput({ panels: this.panels$.value }) } }) ); @@ -561,8 +574,6 @@ export class DashboardContainer duplicateDashboardPanel.bind(this)(id); } - public canRemovePanels = () => this.expandedPanelId.value === undefined; - public getTypeDisplayName = () => dashboardTypeDisplayName; public getTypeDisplayNameLowerCase = () => dashboardTypeDisplayLowercase; @@ -570,6 +581,7 @@ export class DashboardContainer public expandedPanelId: BehaviorSubject; public focusedPanelId$: BehaviorSubject; public managed$: BehaviorSubject; + public children$: BehaviorSubject<{ [key: string]: unknown }>; public fullScreenMode$: BehaviorSubject; public hasRunMigrations$: BehaviorSubject; public hasUnsavedChanges$: BehaviorSubject; @@ -578,7 +590,7 @@ export class DashboardContainer public scrollToPanelId$: BehaviorSubject; public highlightPanelId$: BehaviorSubject; public animatePanelTransforms$: BehaviorSubject; - public panels$: BehaviorSubject; + public panels$: PublishingSubject; public isEmbeddedExternally: boolean; public uuid$: BehaviorSubject; @@ -600,6 +612,10 @@ export class DashboardContainer panelPackage: PanelPackage, displaySuccessMessage?: boolean ) { + if (embeddableService.reactEmbeddableRegistryHasKey(panelPackage.panelType)) { + return await this.dashboardApi.addNewPanel(panelPackage, displaySuccessMessage); + } + const onSuccess = (id?: string, title?: string) => { if (!displaySuccessMessage) return; coreServices.notifications.toasts.addSuccess({ @@ -613,48 +629,6 @@ export class DashboardContainer if (this.trackPanelAddMetric) { this.trackPanelAddMetric(METRIC_TYPE.CLICK, panelPackage.panelType); } - if (embeddableService.reactEmbeddableRegistryHasKey(panelPackage.panelType)) { - const newId = v4(); - - const getCustomPlacementSettingFunc = getDashboardPanelPlacementSetting( - panelPackage.panelType - ); - - const customPlacementSettings = getCustomPlacementSettingFunc - ? await getCustomPlacementSettingFunc(panelPackage.initialState) - : {}; - - const placementSettings = { - width: DEFAULT_PANEL_WIDTH, - height: DEFAULT_PANEL_HEIGHT, - strategy: PanelPlacementStrategy.findTopLeftMostOpenSpace, - ...customPlacementSettings, - }; - - const { width, height, strategy } = placementSettings; - - const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(strategy, { - currentPanels: this.getInput().panels, - height, - width, - }); - const newPanel: DashboardPanelState = { - type: panelPackage.panelType, - gridData: { - ...newPanelPlacement, - i: newId, - }, - explicitInput: { - id: newId, - }, - }; - if (panelPackage.initialState) { - this.setRuntimeStateForChild(newId, panelPackage.initialState); - } - this.updateInput({ panels: { ...otherPanels, [newId]: newPanel } }); - onSuccess(newId, newPanel.explicitInput.title); - return await this.untilReactEmbeddableLoaded(newId); - } const embeddableFactory = embeddableService.getEmbeddableFactory(panelPackage.panelType); if (!embeddableFactory) { @@ -739,7 +713,7 @@ export class DashboardContainer if (timeRange) timeFilterService.setTime(timeRange); if (refreshInterval) timeFilterService.setRefreshInterval(refreshInterval); } - this.resetAllReactEmbeddables(); + this.dashboardApi.resetAllReactEmbeddables(); } public navigateToDashboard = async ( @@ -857,6 +831,7 @@ export class DashboardContainer } public setPanels = (panels: DashboardPanelMap) => { + // panels$ gets updated via subscription to input$ this.dispatch.setPanels(panels); }; @@ -872,17 +847,6 @@ export class DashboardContainer public saveNotification$: Subject = new Subject(); - public getSerializedStateForChild = (childId: string) => { - const rawState = this.getInput().panels[childId].explicitInput; - const { id, ...serializedState } = rawState; - if (!rawState || Object.keys(serializedState).length === 0) return; - const references = getReferencesForPanelId(childId, this.savedObjectReferences); - return { - rawState, - references, - }; - }; - public getSerializedStateForControlGroup = () => { return { rawState: this.controlGroupInput @@ -899,29 +863,10 @@ export class DashboardContainer }; }; - private restoredRuntimeState: UnsavedPanelState | undefined = undefined; - public setRuntimeStateForChild = (childId: string, state: object) => { - const runtimeState = this.restoredRuntimeState ?? {}; - runtimeState[childId] = state; - this.restoredRuntimeState = runtimeState; - }; - public getRuntimeStateForChild = (childId: string) => { - return this.restoredRuntimeState?.[childId]; - }; - public getRuntimeStateForControlGroup = () => { return this.getRuntimeStateForChild(PANELS_CONTROL_GROUP_KEY); }; - public removePanel(id: string) { - const type = this.getInput().panels[id]?.type; - this.removeEmbeddable(id); - if (embeddableService.reactEmbeddableRegistryHasKey(type)) { - const { [id]: childToRemove, ...otherChildren } = this.children$.value; - this.children$.next(otherChildren); - } - } - public startAuditingReactEmbeddableChildren = () => { const auditChildren = () => { const currentChildren = this.children$.value; @@ -946,21 +891,4 @@ export class DashboardContainer ); auditChildren(); }; - - public resetAllReactEmbeddables = () => { - this.restoredRuntimeState = undefined; - let resetChangedPanelCount = false; - const currentChildren = this.children$.value; - for (const panelId of Object.keys(currentChildren)) { - if (this.getInput().panels[panelId]) { - const child = currentChildren[panelId]; - if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges(); - } else { - // if reset resulted in panel removal, we need to update the list of children - delete currentChildren[panelId]; - resetChangedPanelCount = true; - } - } - if (resetChangedPanelCount) this.children$.next(currentChildren); - }; } diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 8e7487323cf44..0ebf3aeb0d4e6 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -54,14 +54,10 @@ export abstract class Container< TContainerOutput extends ContainerOutput = ContainerOutput > extends Embeddable - implements IContainer, PresentationContainer + implements IContainer, Omit { public readonly isContainer: boolean = true; - public children$: BehaviorSubject<{ [key: string]: unknown }> = new BehaviorSubject<{ - [key: string]: unknown; - }>({}); - private subscription: Subscription | undefined; private readonly anyChildOutputChange$; @@ -123,6 +119,14 @@ export abstract class Container< ); } + protected getChildren(): { [key: string]: unknown } { + throw new Error('getChildren implemenation not provided') + } + + protected setChildren(children: { [key: string]: unknown }): void { + throw new Error('getChildren implemenation not provided') + } + public getPanelCount() { return Object.keys(this.getInput().panels).length; } @@ -157,8 +161,8 @@ export abstract class Container< return; } - const currentChildren = this.children$.value; - this.children$.next({ + const currentChildren = this.getChildren; + this.setChildren({ ...currentChildren, [embeddable.id]: embeddable, }); @@ -193,7 +197,7 @@ export abstract class Container< } public reload() { - for (const child of Object.values(this.children$.value)) { + for (const child of Object.values(this.getChildren())) { (child as IEmbeddable)?.reload?.(); } } @@ -280,11 +284,11 @@ export abstract class Container< } public getChildIds(): string[] { - return Object.keys(this.children$.value); + return Object.keys(this.getChildren()); } public getChild(id: string): E { - return this.children$.value[id] as E; + return this.getChildren()[id] as E; } public getInputForChild( @@ -325,7 +329,7 @@ export abstract class Container< public destroy() { super.destroy(); - for (const child of Object.values(this.children$.value)) { + for (const child of Object.values(this.getChildren())) { (child as IEmbeddable)?.destroy?.(); } this.subscription?.unsubscribe(); @@ -339,14 +343,14 @@ export abstract class Container< } if (this.output.embeddableLoaded[id]) { - return this.children$.value[id] as TEmbeddable; + return this.getChildren()[id] as TEmbeddable; } return new Promise((resolve, reject) => { const subscription = merge(this.getOutput$(), this.getInput$()).subscribe(() => { if (this.output.embeddableLoaded[id]) { subscription.unsubscribe(); - resolve(this.children$.value[id] as TEmbeddable); + resolve(this.getChildren()[id] as TEmbeddable); } // If we hit this, the panel was removed before the embeddable finished loading. @@ -359,31 +363,6 @@ export abstract class Container< }); } - public async untilReactEmbeddableLoaded(id: string): Promise { - if (!this.input.panels[id]) { - throw new PanelNotFoundError(); - } - - if (this.children$.value[id]) { - return this.children$.value[id] as ApiType; - } - - return new Promise((resolve, reject) => { - const subscription = merge(this.children$, this.getInput$()).subscribe(() => { - if (this.children$.value[id]) { - subscription.unsubscribe(); - resolve(this.children$.value[id] as ApiType); - } - - // If we hit this, the panel was removed before the embeddable finished loading. - if (this.input.panels[id] === undefined) { - subscription.unsubscribe(); - resolve(undefined); - } - }); - }); - } - public async getExplicitInputIsEqual(lastInput: TContainerInput) { const { panels: lastPanels, ...restOfLastInput } = lastInput; const { panels: currentPanels, ...restOfCurrentInput } = this.getExplicitInput(); @@ -534,9 +513,9 @@ export abstract class Container< embeddable.destroy(); // Remove references. - const nextChildren = this.children$.value; + const nextChildren = this.getChildren(); delete nextChildren[id]; - this.children$.next(nextChildren); + this.setChildren(nextChildren); } this.updateOutput({