diff --git a/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.ts b/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.ts index 452837650d814..f7b529ead69dc 100644 --- a/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.ts +++ b/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/initialize_unsaved_changes.ts @@ -32,7 +32,7 @@ export const initializeUnsavedChanges = ; serializeState: () => SerializedPanelState; getComparators: () => StateComparators; - onReset: (lastSavedState?: SerializedPanelState) => MaybePromise; + onReset: (lastSavedPanelState?: SerializedPanelState) => MaybePromise; }): PublishesUnsavedChanges => { if (!apiHasLastSavedChildState(parentApi)) { return { diff --git a/x-pack/platform/plugins/shared/lens/public/plugin.ts b/x-pack/platform/plugins/shared/lens/public/plugin.ts index 961ab5da19dcb..31e620a1333ad 100644 --- a/x-pack/platform/plugins/shared/lens/public/plugin.ts +++ b/x-pack/platform/plugins/shared/lens/public/plugin.ts @@ -399,21 +399,14 @@ export class LensPlugin { // Let Dashboard know about the Lens panel type embeddable.registerAddFromLibraryType({ onAdd: async (container, savedObject) => { - const [services, { deserializeState }] = await Promise.all([ - getStartServicesForEmbeddable(), - import('./async_services'), - ]); - // deserialize the saved object from visualize library - // this make sure to fit into the new embeddable model, where the following build() - // function expects a fully loaded runtime state - const state = await deserializeState( - services, - { savedObjectId: savedObject.id }, - savedObject.references - ); container.addNewPanel({ panelType: LENS_EMBEDDABLE_TYPE, - initialState: state, + serializedState: { + rawState: { + savedObjectId: savedObject.id, + }, + references: savedObject.references + }, }); }, savedObjectType: LENS_EMBEDDABLE_TYPE, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts index adf382f741da5..e1fe24667fcfe 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.test.ts @@ -37,7 +37,6 @@ import { isObject } from 'lodash'; import { createMockDatasource, defaultDoc } from '../mocks'; import { ESQLVariableType, type ESQLControlVariable } from '@kbn/esql-types'; import * as Logger from './logger'; -import { buildObservableVariable } from './helper'; jest.mock('@kbn/interpreter', () => ({ toExpression: jest.fn().mockReturnValue('expression'), @@ -132,7 +131,7 @@ async function expectRerenderOnDataLoader( const getState = jest.fn(() => runtimeState); const internalApi = getLensInternalApiMock({ ...internalApiOverrides, - attributes$: buildObservableVariable(runtimeState.attributes)[0], + attributes$: new BehaviorSubject(runtimeState.attributes), }); const services = { ...makeEmbeddableServices(new BehaviorSubject(''), undefined, { @@ -567,7 +566,7 @@ describe('Data Loader', () => { }, { internalApiOverrides: { - esqlVariables$: buildObservableVariable(variables)[0], + esqlVariables$: new BehaviorSubject(variables), }, } ); diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts index faabd2a7d88b4..62b3718f3170b 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/data_loader.ts @@ -31,7 +31,7 @@ import { buildUserMessagesHelpers } from './user_messages/api'; import { getLogError } from './expressions/telemetry'; import type { SharingSavedObjectProps, UserMessagesDisplayLocationId } from '../types'; import { apiHasLensComponentCallbacks } from './type_guards'; -import { getRenderMode, getParentContext, buildObservableVariable } from './helper'; +import { getRenderMode, getParentContext } from './helper'; import { addLog } from './logger'; import { getUsedDataViews } from './expressions/update_data_views'; import { getMergedSearchContext } from './expressions/merged_search_context'; @@ -119,7 +119,7 @@ export function loadEmbeddableData( } }; - const [controlESQLVariables$] = buildObservableVariable([]); + const controlESQLVariables$ = new BehaviorSubject([]); async function reload( // make reload easier to debug diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts index a4b1fd2a714a2..65e6014b774b0 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts @@ -15,7 +15,6 @@ import { } from '@kbn/presentation-publishing'; import { isObject } from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import fastIsEqual from 'fast-deep-equal'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { RenderMode } from '@kbn/expressions-plugin/common'; import { SavedObjectReference } from '@kbn/core/types'; @@ -99,39 +98,6 @@ export async function deserializeState( return newState; } -export function emptySerializer() { - return {}; -} - -export type ComparatorType = [ - BehaviorSubject, - (newValue: T) => void, - (a: T, b: T) => boolean -]; - -export function makeComparator( - observable: BehaviorSubject -): ComparatorType { - return [observable, (newValue: T) => observable.next(newValue), fastIsEqual]; -} - -/** - * Helper function to either extract an observable from an API or create a new one - * with a default value to start with. - * Note that extracting from the API will make subscription emit if the value changes upstream - * as it keeps the original reference without cloning. - * @returns the observable and a comparator to use for detecting "unsaved changes" on it - */ -export function buildObservableVariable( - variable: T | PublishingSubject -): [BehaviorSubject, ComparatorType] { - if (variable instanceof BehaviorSubject) { - return [variable, makeComparator(variable)]; - } - const variable$ = new BehaviorSubject(variable as T); - return [variable$, makeComparator(variable$)]; -} - export function isTextBasedLanguage(state: LensRuntimeState) { return isOfAggregateQueryType(state.attributes?.state.query); } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.ts index 4c1b811ad8b2f..bb68375d244e2 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_actions.ts @@ -20,7 +20,6 @@ import { PublishingSubject, StateComparators, apiPublishesUnifiedSearch, - getUnchangingComparator, } from '@kbn/presentation-publishing'; import { HasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public'; import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin'; @@ -31,16 +30,18 @@ import { TableInspectorAdapter } from '../../editor_frame_service/types'; import { Datasource, IndexPatternMap } from '../../types'; import { getMergedSearchContext } from '../expressions/merged_search_context'; -import { buildObservableVariable, isTextBasedLanguage } from '../helper'; +import { isTextBasedLanguage } from '../helper'; import type { GetStateType, LensEmbeddableStartServices, LensInternalApi, LensRuntimeState, + LensSerializedState, ViewInDiscoverCallbacks, ViewUnderlyingDataArgs, } from '../types'; import { getActiveDatasourceIdFromDoc, getActiveVisualizationIdFromDoc } from '../../utils'; +import { BehaviorSubject, Observable } from 'rxjs'; function getViewUnderlyingDataArgs({ activeDatasource, @@ -212,7 +213,7 @@ function createViewUnderlyingDataApis( ): ViewInDiscoverCallbacks { let viewUnderlyingDataArgs: undefined | ViewUnderlyingDataArgs; - const [canViewUnderlyingData$] = buildObservableVariable(false); + const canViewUnderlyingData$ = new BehaviorSubject(false); return { canViewUnderlyingData$, @@ -247,20 +248,22 @@ export function initializeActionApi( services: LensEmbeddableStartServices ): { api: ViewInDiscoverCallbacks & HasDynamicActions; - comparators: StateComparators; - serialize: () => {}; + anyStateChange$: Observable; + getComparators: () => StateComparators; + getLatestState: () => DynamicActionsSerializedState; cleanup: () => void; + reinitializeState: (lastSaved?: LensSerializedState) => void; } { - const dynamicActionsApi = services.embeddableEnhanced?.initializeReactEmbeddableDynamicActions( + const dynamicActionsManger = services.embeddableEnhanced?.initializeEmbeddableDynamicActions( uuid, () => title$.getValue(), initialState ); - const maybeStopDynamicActions = dynamicActionsApi?.startDynamicActions(); + const maybeStopDynamicActions = dynamicActionsManger?.startDynamicActions(); return { api: { - ...(isTextBasedLanguage(initialState) ? {} : dynamicActionsApi?.dynamicActionsApi ?? {}), + ...(isTextBasedLanguage(initialState) ? {} : dynamicActionsManger?.api ?? {}), ...createViewUnderlyingDataApis( getLatestState, internalApi, @@ -269,14 +272,18 @@ export function initializeActionApi( services ), }, - comparators: { - ...(dynamicActionsApi?.dynamicActionsComparator ?? { - enhancements: getUnchangingComparator(), + anyStateChange$: dynamicActionsManger?.anyStateChange$ ?? new BehaviorSubject(undefined), + getComparators: () => ({ + ...(dynamicActionsManger?.comparators ?? { + enhancements: 'skip', }), - }, - serialize: () => dynamicActionsApi?.serializeDynamicActions() ?? {}, + }), + getLatestState: () => dynamicActionsManger?.getLatestState ?? {}, cleanup: () => { maybeStopDynamicActions?.stopDynamicActions(); }, + reinitializeState: (lastSaved?: LensSerializedState) => { + dynamicActionsManger?.reinitializeState(lastSaved ?? {}); + }, }; } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts index 1f8191ca33386..efdec003f37e1 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_dashboard_services.ts @@ -12,11 +12,12 @@ import { PublishesWritableDescription, SerializedTitles, StateComparators, - getUnchangingComparator, initializeTitleManager, + titleComparators, } from '@kbn/presentation-publishing'; import { apiIsPresentationContainer, apiPublishesSettings } from '@kbn/presentation-containers'; -import { buildObservableVariable, isTextBasedLanguage } from '../helper'; +import { BehaviorSubject, Observable, map, merge } from 'rxjs'; +import { isTextBasedLanguage } from '../helper'; import type { LensComponentProps, LensPanelProps, @@ -35,17 +36,33 @@ import { StateManagementConfig } from './initialize_state_management'; // Convenience type for the serialized props of this initializer type SerializedProps = SerializedTitles & LensPanelProps & LensOverrides & LensSharedProps; +export const dashboardServicesComparators: StateComparators = { + ...titleComparators, + disableTriggers: 'referenceEquality', + overrides: 'referenceEquality', + id: 'skip', + palette: 'skip', + renderMode: 'skip', + syncColors: 'skip', + syncCursor: 'skip', + syncTooltips: 'skip', + executionContext: 'skip', + noPadding: 'skip', + viewMode: 'skip', + style: 'skip', + className: 'skip', + forceDSL: 'skip', +}; + export interface DashboardServicesConfig { api: PublishesWritableTitle & PublishesWritableDescription & HasLibraryTransforms & Pick & Pick; - serialize: () => SerializedProps; - comparators: StateComparators< - SerializedProps & Pick & { isNewPanel?: boolean } - >; - cleanup: () => void; + anyStateChange$: Observable; + getLatestState: () => SerializedProps; + reinitializeState: (lastSaved?: LensSerializedState) => void; } /** @@ -62,22 +79,14 @@ export function initializeDashboardServices( ): DashboardServicesConfig { // For some legacy reason the title and description default value is picked differently // ( based on existing FTR tests ). - const [defaultTitle$] = buildObservableVariable( + const defaultTitle$ = new BehaviorSubject( initialState.title || internalApi.attributes$.getValue().title ); - const [defaultDescription$] = buildObservableVariable( + const defaultDescription$ = new BehaviorSubject( initialState.savedObjectId ? internalApi.attributes$.getValue().description || initialState.description : initialState.description ); - // The observable references here are the same to the internalApi, - // the buildObservableVariable re-uses the same observable when detected but it builds the right comparator - const [overrides$, overridesComparator] = buildObservableVariable( - internalApi.overrides$ - ); - const [disableTriggers$, disabledTriggersComparator] = buildObservableVariable< - boolean | undefined - >(internalApi.disableTriggers$); return { api: { @@ -131,7 +140,10 @@ export function initializeDashboardServices( return attributeService.extractReferences(byValueRuntimeState); }, }, - serialize: () => { + anyStateChange$: merge(internalApi.overrides$, internalApi.disableTriggers$).pipe( + map(() => undefined) + ), + getLatestState: () => { const { style, className } = apiHasLensComponentProps(parentApi) ? parentApi : ({} as LensComponentProps); @@ -143,34 +155,18 @@ export function initializeDashboardServices( } : {}; return { - ...titleManager.serialize(), + ...titleManager.getLatestState(), style, className, ...settings, palette: initialState.palette, - overrides: overrides$.getValue(), - disableTriggers: disableTriggers$.getValue(), + overrides: internalApi.overrides$.getValue(), + disableTriggers: internalApi.disableTriggers$.getValue(), }; }, - comparators: { - ...titleManager.comparators, - id: getUnchangingComparator(), - palette: getUnchangingComparator(), - renderMode: getUnchangingComparator(), - syncColors: getUnchangingComparator(), - syncCursor: getUnchangingComparator(), - syncTooltips: getUnchangingComparator(), - executionContext: getUnchangingComparator(), - noPadding: getUnchangingComparator(), - viewMode: getUnchangingComparator(), - style: getUnchangingComparator(), - className: getUnchangingComparator(), - overrides: overridesComparator, - disableTriggers: disabledTriggersComparator, - forceDSL: getUnchangingComparator(), - isNewPanel: getUnchangingComparator<{ isNewPanel?: boolean }, 'isNewPanel'>(), - parentApi: getUnchangingComparator, 'parentApi'>(), + reinitializeState: (lastSaved?: LensSerializedState) => { + internalApi.updateDisabledTriggers(lastSaved?.disableTriggers); + internalApi.updateOverrides(lastSaved?.overrides); }, - cleanup: noop, }; } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx index 7f10d03c976c3..0ed63fc2df4e6 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_edit.tsx @@ -12,6 +12,7 @@ import { PublishesDisabledActionIds, PublishesViewMode, PublishingSubject, + StateComparators, ViewMode, apiHasAppContext, apiPublishesDisabledActionIds, @@ -21,7 +22,7 @@ import { noop } from 'lodash'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { tracksOverlays } from '@kbn/presentation-containers'; import { i18n } from '@kbn/i18n'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { APP_ID, getEditPath } from '../../../common/constants'; import { GetStateType, @@ -29,10 +30,9 @@ import { LensInspectorAdapters, LensInternalApi, LensRuntimeState, + LensSerializedState, } from '../types'; import { - buildObservableVariable, - emptySerializer, extractInheritedViewModeObservable, } from '../helper'; import { prepareInlineEditPanel } from '../inline_editing/setup_inline_editing'; @@ -87,9 +87,6 @@ export function initializeEditApi( HasEditCapabilities & HasReadOnlyCapabilities & PublishesViewMode & { uuid: string }; - comparators: {}; - serialize: () => {}; - cleanup: () => void; } { const supportedTriggers = getSupportedTriggers(getState, startDependencies.visualizationMap); const isManaged = (currentState: LensRuntimeState) => { @@ -98,9 +95,7 @@ export function initializeEditApi( const isESQLModeEnabled = () => uiSettings.get(ENABLE_ESQL); - const [viewMode$] = buildObservableVariable( - extractInheritedViewModeObservable(parentApi) - ); + const viewMode$ = extractInheritedViewModeObservable(parentApi); const { disabledActionIds$, setDisabledActionIds } = apiPublishesDisabledActionIds(parentApi) ? parentApi @@ -247,9 +242,6 @@ export function initializeEditApi( }; return { - comparators: { disabledActionIds$: [disabledActionIds$, setDisabledActionIds] }, - serialize: emptySerializer, - cleanup: noop, api: { uuid, viewMode$, diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_inspector.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_inspector.ts index 733a1d4eac46c..ed166667daf12 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_inspector.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_inspector.ts @@ -5,18 +5,13 @@ * 2.0. */ -import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; import type { Adapters } from '@kbn/inspector-plugin/public'; import { getLensInspectorService } from '../../lens_inspector_service'; -import { emptySerializer } from '../helper'; import type { LensEmbeddableStartServices, LensInspectorAdapters } from '../types'; export function initializeInspector(services: LensEmbeddableStartServices): { api: LensInspectorAdapters; - comparators: {}; - serialize: () => {}; - cleanup: () => void; } { const inspectorApi = getLensInspectorService(services.inspector); @@ -25,8 +20,5 @@ export function initializeInspector(services: LensEmbeddableStartServices): { ...inspectorApi, adapters$: new BehaviorSubject(inspectorApi.getInspectorAdapters()), }, - comparators: {}, - serialize: emptySerializer, - cleanup: noop, }; } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts index 1fb7556071fcf..39ad1e0752405 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts @@ -10,10 +10,10 @@ import { getLanguageDisplayName, isOfAggregateQueryType, } from '@kbn/es-query'; -import { noop, omit } from 'lodash'; +import { omit } from 'lodash'; import type { HasSerializableState } from '@kbn/presentation-publishing'; import { SavedObjectReference } from '@kbn/core/types'; -import { emptySerializer, isTextBasedLanguage } from '../helper'; +import { isTextBasedLanguage } from '../helper'; import type { GetStateType, LensEmbeddableStartServices, LensRuntimeState } from '../types'; import type { IntegrationCallbacks } from '../types'; @@ -46,9 +46,6 @@ export function initializeIntegrations( | 'getTriggerCompatibleActions' > & HasSerializableState; - cleanup: () => void; - serialize: () => {}; - comparators: {}; } { return { api: { @@ -79,8 +76,5 @@ export function initializeIntegrations( return getLanguageDisplayName(language).toUpperCase(); }, }, - comparators: {}, - serialize: emptySerializer, - cleanup: noop, }; } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts index e34ea644c02d4..7aa7aad19535b 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_internal_api.ts @@ -9,12 +9,13 @@ import { BehaviorSubject } from 'rxjs'; import { initializeTitleManager } from '@kbn/presentation-publishing'; import { apiPublishesESQLVariables } from '@kbn/esql-types'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { buildObservableVariable, createEmptyLensState } from '../helper'; +import { createEmptyLensState } from '../helper'; import type { ExpressionWrapperProps, LensEmbeddableStartServices, LensInternalApi, LensOverrides, + LensPanelProps, LensRuntimeState, VisualizationContext, } from '../types'; @@ -27,13 +28,13 @@ export function initializeInternalApi( titleManager: ReturnType, { visualizationMap }: LensEmbeddableStartServices ): LensInternalApi { - const [hasRenderCompleted$] = buildObservableVariable(false); - const [expressionParams$] = buildObservableVariable(null); + const hasRenderCompleted$ = new BehaviorSubject(false); + const expressionParams$ = new BehaviorSubject(null); const expressionAbortController$ = new BehaviorSubject(undefined); if (apiHasAbortController(parentApi)) { expressionAbortController$.next(parentApi.abortController); } - const [renderCount$] = buildObservableVariable(0); + const renderCount$ = new BehaviorSubject(0); const attributes$ = new BehaviorSubject( initialState.attributes || createEmptyLensState().attributes @@ -67,7 +68,7 @@ export function initializeInternalApi( activeData: undefined, }); - const [esqlVariables$] = buildObservableVariable( + const esqlVariables$ = new BehaviorSubject( apiPublishesESQLVariables(parentApi) ? parentApi.esqlVariables$ : [] ); @@ -104,6 +105,7 @@ export function initializeInternalApi( updateAttributes: (attributes: LensRuntimeState['attributes']) => attributes$.next(attributes), updateAbortController: (abortController: AbortController | undefined) => expressionAbortController$.next(abortController), + updateDisabledTriggers: (disableTriggers: LensPanelProps['disableTriggers']) => disableTriggers$.next(disableTriggers), updateDataViews: (dataViews: DataView[] | undefined) => dataViews$.next(dataViews), updateMessages: (newMessages: UserMessage[]) => messages$.next(newMessages), updateValidationMessages: (newMessages: UserMessage[]) => validationMessages$.next(newMessages), diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_search_context.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_search_context.ts index 1d95fd49b3f55..b5d15b2f64073 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_search_context.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_search_context.ts @@ -9,27 +9,36 @@ import { Filter, Query, AggregateQuery } from '@kbn/es-query'; import { PublishesUnifiedSearch, StateComparators, - getUnchangingComparator, - initializeTimeRange, + initializeTimeRangeManager, + timeRangeComparators, } from '@kbn/presentation-publishing'; -import { noop } from 'lodash'; import { PublishesSearchSession, apiPublishesSearchSession, } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; -import { buildObservableVariable } from '../helper'; import { LensEmbeddableStartServices, LensInternalApi, LensRuntimeState, + LensSerializedState, LensUnifiedSearchContext, } from '../types'; +import { BehaviorSubject, Observable, merge } from 'rxjs'; + +export const searchContextComparators: StateComparators = { + ...timeRangeComparators, + query: 'skip', + filters: 'skip', + timeslice: 'skip', + searchSessionId: 'skip', + lastReloadRequestTime: 'skip', +}; export interface SearchContextConfig { api: PublishesUnifiedSearch & PublishesSearchSession; - comparators: StateComparators; - serialize: () => LensUnifiedSearchContext; - cleanup: () => void; + anyStateChange$: Observable; + getLatestState: () => LensUnifiedSearchContext; + reinitializeState: (lastSaved?: LensSerializedState) => void; } export function initializeSearchContext( @@ -38,26 +47,26 @@ export function initializeSearchContext( parentApi: unknown, { injectFilterReferences }: LensEmbeddableStartServices ): SearchContextConfig { - const [searchSessionId$] = buildObservableVariable( - apiPublishesSearchSession(parentApi) ? parentApi.searchSessionId$ : undefined - ); + const searchSessionId$ = apiPublishesSearchSession(parentApi) + ? parentApi.searchSessionId$ + : new BehaviorSubject(undefined); const attributes = internalApi.attributes$.getValue(); - const [lastReloadRequestTime] = buildObservableVariable(undefined); + const lastReloadRequestTime$ = new BehaviorSubject(undefined); // Make sure the panel access the filters with the correct references - const [filters$] = buildObservableVariable( + const filters$ = new BehaviorSubject( injectFilterReferences(attributes.state.filters, attributes.references) ); - const [query$] = buildObservableVariable( + const query$ = new BehaviorSubject( attributes.state.query ); - const [timeslice$] = buildObservableVariable<[number, number] | undefined>(undefined); + const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined); - const timeRange = initializeTimeRange(initialState); + const timeRangeManager = initializeTimeRangeManager(initialState); return { api: { searchSessionId$, @@ -65,27 +74,21 @@ export function initializeSearchContext( query$, timeslice$, isCompatibleWithUnifiedSearch: () => true, - ...timeRange.api, + ...timeRangeManager.api, }, - comparators: { - query: getUnchangingComparator(), - filters: getUnchangingComparator(), - timeslice: getUnchangingComparator(), - searchSessionId: getUnchangingComparator(), - lastReloadRequestTime: getUnchangingComparator< - LensUnifiedSearchContext, - 'lastReloadRequestTime' - >(), - ...timeRange.comparators, - }, - cleanup: noop, - serialize: () => ({ + anyStateChange$: merge( + timeRangeManager.anyStateChange$, + ), + getLatestState: () => ({ searchSessionId: searchSessionId$.getValue(), filters: filters$.getValue(), query: query$.getValue(), timeslice: timeslice$.getValue(), - lastReloadRequestTime: lastReloadRequestTime.getValue(), - ...timeRange.serialize(), + lastReloadRequestTime: lastReloadRequestTime$.getValue(), + ...timeRangeManager.getLatestState(), }), + reinitializeState: (lastSaved?: LensSerializedState) => { + timeRangeManager.reinitializeState(lastSaved); + }, }; } diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts index b085f84eb5267..a677bf8a5dc55 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_state_management.ts @@ -5,20 +5,16 @@ * 2.0. */ import { - getUnchangingComparator, type PublishesBlockingError, type PublishesDataLoading, type PublishesDataViews, type PublishesSavedObjectId, - type StateComparators, type PublishesRendered, + StateComparators, } from '@kbn/presentation-publishing'; import { noop } from 'lodash'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import { BehaviorSubject } from 'rxjs'; -import type { IntegrationCallbacks, LensInternalApi, LensRuntimeState } from '../types'; -import { buildObservableVariable } from '../helper'; -import { SharingSavedObjectProps } from '../../types'; +import { BehaviorSubject, Observable, map, merge } from 'rxjs'; +import type { IntegrationCallbacks, LensInternalApi, LensRuntimeState, LensSerializedState } from '../types'; export interface StateManagementConfig { api: Pick & @@ -27,13 +23,10 @@ export interface StateManagementConfig { PublishesDataLoading & PublishesRendered & PublishesBlockingError; - serialize: () => Pick; - comparators: StateComparators< - Pick & { - managed?: boolean | undefined; - sharingSavedObjectProps?: SharingSavedObjectProps | undefined; - } - >; + anyStateChange$: Observable; + getComparators: () => StateComparators> + reinitializeRuntimeState: (lastSavedRuntimeState: LensRuntimeState) => void; + getLatestState: () => Pick; cleanup: () => void; } @@ -45,58 +38,39 @@ export function initializeStateManagement( initialState: LensRuntimeState, internalApi: LensInternalApi ): StateManagementConfig { - const [attributes$, attributesComparator] = buildObservableVariable< - LensRuntimeState['attributes'] - >(internalApi.attributes$); - - const [savedObjectId$, savedObjectIdComparator] = buildObservableVariable< - LensRuntimeState['savedObjectId'] - >(initialState.savedObjectId); - - const [dataViews$] = buildObservableVariable(internalApi.dataViews$); - const [dataLoading$] = buildObservableVariable(internalApi.dataLoading$); - const [rendered$] = buildObservableVariable(internalApi.hasRenderCompleted$); - const [abortController$, abortControllerComparator] = buildObservableVariable< - AbortController | undefined - >(internalApi.expressionAbortController$); - - // This is the way to communicate to the embeddable panel to render a blocking error with the - // default panel error component - i.e. cannot find a Lens SO type of thing. - // For Lens specific errors, we use a Lens specific error component. - const [blockingError$] = buildObservableVariable(internalApi.blockingError$); + const savedObjectId$ = new BehaviorSubject(initialState.savedObjectId) + return { api: { updateAttributes: internalApi.updateAttributes, updateSavedObjectId: (newSavedObjectId: LensRuntimeState['savedObjectId']) => savedObjectId$.next(newSavedObjectId), savedObjectId$, - dataViews$, - dataLoading$, - blockingError$, - rendered$, + dataViews$: internalApi.dataViews$, + dataLoading$: internalApi.dataLoading$, + blockingError$: internalApi.blockingError$, + rendered$: internalApi.hasRenderCompleted$, }, - serialize: () => { + anyStateChange$: merge( + internalApi.attributes$, + ).pipe(map(() => undefined)), + getComparators: () => { return { - attributes: attributes$.getValue(), + attributes: initialState.savedObjectId === undefined + ? 'deepEquality' + : 'skip', + savedObjectId: 'skip', + } + }, + getLatestState: () => { + return { + attributes: internalApi.attributes$.getValue(), savedObjectId: savedObjectId$.getValue(), - abortController: abortController$.getValue(), }; }, - comparators: { - // need to force cast this to make it pass the type check - // @TODO: workout why this is needed - attributes: attributesComparator as [ - BehaviorSubject, - (newValue: LensRuntimeState['attributes'] | undefined) => void, - ( - a: LensRuntimeState['attributes'] | undefined, - b: LensRuntimeState['attributes'] | undefined - ) => boolean - ], - savedObjectId: savedObjectIdComparator, - abortController: abortControllerComparator, - sharingSavedObjectProps: getUnchangingComparator(), - managed: getUnchangingComparator(), + reinitializeRuntimeState: (lastSavedRuntimeState: LensRuntimeState) => { + internalApi.updateAttributes(lastSavedRuntimeState.attributes); + }, cleanup: noop, }; diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx index 29c0d7a7a5653..277c1d969bc39 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { initializeTitleManager } from '@kbn/presentation-publishing'; import { DOC_TYPE } from '../../common/constants'; import { @@ -20,25 +20,21 @@ import { loadEmbeddableData } from './data_loader'; import { isTextBasedLanguage, deserializeState } from './helper'; import { initializeEditApi } from './initializers/initialize_edit'; import { initializeInspector } from './initializers/initialize_inspector'; -import { initializeDashboardServices } from './initializers/initialize_dashboard_services'; +import { dashboardServicesComparators, initializeDashboardServices } from './initializers/initialize_dashboard_services'; import { initializeInternalApi } from './initializers/initialize_internal_api'; -import { initializeSearchContext } from './initializers/initialize_search_context'; +import { initializeSearchContext, searchContextComparators } from './initializers/initialize_search_context'; import { initializeActionApi } from './initializers/initialize_actions'; import { initializeIntegrations } from './initializers/initialize_integrations'; import { initializeStateManagement } from './initializers/initialize_state_management'; import { LensEmbeddableComponent } from './renderer/lens_embeddable_component'; +import { initializeUnsavedChanges } from '@kbn/presentation-containers'; +import { merge } from 'rxjs'; export const createLensEmbeddableFactory = ( services: LensEmbeddableStartServices -): ReactEmbeddableFactory => { +): EmbeddableFactory => { return { type: DOC_TYPE, - /** - * This is called before the build and will make sure that the - * final state will contain the attributes object - */ - deserializeState: async ({ rawState, references }) => - deserializeState(services, rawState, references), /** * This is called after the deserialize, so some assumptions can be made about its arguments: * @param state the Lens "runtime" state, which means that 'attributes' is always present. @@ -54,31 +50,25 @@ export const createLensEmbeddableFactory = ( * from the Lens component container to the Lens embeddable. * @returns an object with the Lens API and the React component to render in the Embeddable */ - buildEmbeddable: async (initialState, buildApi, uuid, parentApi) => { - const titleManager = initializeTitleManager(initialState); + buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + const titleManager = initializeTitleManager(initialState.rawState); + + const runtimeState = await deserializeState(services, initialState.rawState, initialState.references); /** * Observables and functions declared here are used internally to store mutating state values * This is an internal API not exposed outside of the embeddable. */ - const internalApi = initializeInternalApi(initialState, parentApi, titleManager, services); + const internalApi = initializeInternalApi(runtimeState, parentApi, titleManager, services); /** * Initialize various configurations required to build all the required * parts for the Lens embeddable. - * Each initialize call returns an object with the following properties: - * - api: a set of methods or observables (also non-serializable) who can be picked up within the component - * - serialize: a serializable subset of the Lens runtime state - * - comparators: a set of comparators to help Dashboard determine if the state has changed since its saved state - * - cleanup: a function to clean up any resources when the component is unmounted - * - * Mind: the getState argument is ok to pass as long as it is lazy evaluated (i.e. called within a function). - * If there's something that should be immediately computed use the "initialState" deserialized variable. */ - const stateConfig = initializeStateManagement(initialState, internalApi); + const stateConfig = initializeStateManagement(runtimeState, internalApi); const dashboardConfig = initializeDashboardServices( - initialState, - getState, + runtimeState, + getLatestState, internalApi, stateConfig, parentApi, @@ -89,7 +79,7 @@ export const createLensEmbeddableFactory = ( const inspectorConfig = initializeInspector(services); const searchContextConfig = initializeSearchContext( - initialState, + runtimeState, internalApi, parentApi, services @@ -97,8 +87,8 @@ export const createLensEmbeddableFactory = ( const editConfig = initializeEditApi( uuid, - initialState, - getState, + runtimeState, + getLatestState, internalApi, stateConfig.api, inspectorConfig.api, @@ -108,11 +98,11 @@ export const createLensEmbeddableFactory = ( parentApi ); - const integrationsConfig = initializeIntegrations(getState, services); + const integrationsConfig = initializeIntegrations(getLatestState, services); const actionsConfig = initializeActionApi( uuid, - initialState, - getState, + runtimeState, + getLatestState, parentApi, searchContextConfig.api, titleManager.api.title$, @@ -124,27 +114,55 @@ export const createLensEmbeddableFactory = ( * This is useful to have always the latest version of the state * at hand when calling callbacks or performing actions */ - function getState(): LensRuntimeState { + function getLatestState(): LensRuntimeState { return { - ...actionsConfig.serialize(), - ...editConfig.serialize(), - ...inspectorConfig.serialize(), - ...dashboardConfig.serialize(), - ...searchContextConfig.serialize(), - ...integrationsConfig.serialize(), - ...stateConfig.serialize(), + ...actionsConfig.getLatestState(), + ...dashboardConfig.getLatestState(), + ...searchContextConfig.getLatestState(), + ...stateConfig.getLatestState(), }; } + const unsavedChangesApi = initializeUnsavedChanges({ + uuid, + parentApi, + serializeState: integrationsConfig.api.serializeState, + anyStateChange$: merge( + actionsConfig.anyStateChange$, + dashboardConfig.anyStateChange$, + stateConfig.anyStateChange$, + searchContextConfig.anyStateChange$, + ), + getComparators: () => { + return { + ...actionsConfig.getComparators(), + ...dashboardServicesComparators, + ...searchContextComparators, + ...stateConfig.getComparators(), + isNewPanel: 'skip', + references: 'skip', + }; + }, + onReset: async (lastSaved) => { + actionsConfig.reinitializeState(lastSaved); + dashboardConfig.reinitializeState(lastSaved?.rawState); + searchContextConfig.reinitializeState(lastSaved?.rawState); + if (!lastSaved) return; + const lastSavedRuntimeState = await deserializeState(services, lastSaved.rawState, lastSaved.references); + stateConfig.reinitializeRuntimeState(lastSavedRuntimeState); + }, + }); + /** * Lens API is the object that can be passed to the final component/renderer and * provide access to the services for and by the outside world */ - const api: LensApi = buildApi( + const api: LensApi = finalizeApi( // Note: the order matters here, so make sure to have the // dashboardConfig who owns the savedObjectId after the // stateConfig one who owns the inline editing { + ...unsavedChangesApi, ...editConfig.api, ...inspectorConfig.api, ...searchContextConfig.api, @@ -152,24 +170,14 @@ export const createLensEmbeddableFactory = ( ...integrationsConfig.api, ...stateConfig.api, ...dashboardConfig.api, - }, - { - ...stateConfig.comparators, - ...editConfig.comparators, - ...inspectorConfig.comparators, - ...searchContextConfig.comparators, - ...actionsConfig.comparators, - ...integrationsConfig.comparators, - ...dashboardConfig.comparators, - } - ); + }); // Compute the expression using the provided parameters // Inside a subscription will be updated based on each unifiedSearch change // and as side effect update few observables as expressionParams$, expressionAbortController$ and renderCount$ with the new values upon updates const expressionConfig = loadEmbeddableData( uuid, - getState, + getLatestState, api, parentApi, internalApi, @@ -177,13 +185,8 @@ export const createLensEmbeddableFactory = ( ); const onUnmount = () => { - editConfig.cleanup(); - inspectorConfig.cleanup(); - searchContextConfig.cleanup(); expressionConfig.cleanup(); actionsConfig.cleanup(); - integrationsConfig.cleanup(); - dashboardConfig.cleanup(); }; return { diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts index b068e5e2944b8..764be2ad286fe 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/types.ts @@ -376,7 +376,7 @@ export interface LensInspectorAdapters { } export type LensApi = Simplify< - DefaultEmbeddableApi & + DefaultEmbeddableApi & // This is used by actions to operate the edit action HasEditCapabilities & // for blocking errors leverage the embeddable panel UI @@ -436,6 +436,7 @@ export type LensInternalApi = Simplify< updateAbortController: (newAbortController: AbortController | undefined) => void; renderCount$: PublishingSubject; updateDataViews: (dataViews: DataView[] | undefined) => void; + updateDisabledTriggers: (disableTriggers: LensPanelProps['disableTriggers']) => void; messages$: PublishingSubject; updateMessages: (newMessages: UserMessage[]) => void; validationMessages$: PublishingSubject;