From 59b1f8ae940312313d7f8276c101ffca478034ad Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 22 Mar 2024 12:28:20 -0400 Subject: [PATCH] [Embeddables Rebuild] Move titles API (#179202) Moves the titles API and state comparators away from the embeddable plugin and to the presentation-publishing package. --- .../eui_markdown_react_embeddable.tsx | 9 ++- .../react_embeddables/eui_markdown/types.ts | 8 +- .../field_list_react_embeddable.tsx | 6 +- .../react_embeddables/field_list/types.ts | 8 +- .../search/build_search_embeddable.tsx | 9 +-- .../comparators/index.ts | 10 +++ .../comparators/state_comparators.ts | 44 +++++++++++ .../comparators/types.ts | 26 +++++++ .../presentation_publishing/index.ts | 73 ++++++++++--------- .../publishes_panel_description.ts | 2 +- .../publishes_panel_title.test.ts | 0 .../{ => titles}/publishes_panel_title.ts | 23 +----- .../interfaces/titles/titles_api.test.ts | 31 ++++---- .../interfaces/titles/titles_api.ts | 41 ++++------- src/plugins/embeddable/public/index.ts | 4 - .../public/react_embeddable_system/index.ts | 6 -- .../react_embeddable_renderer.tsx | 4 +- .../react_embeddable_unsaved_changes.test.tsx | 10 +-- .../react_embeddable_unsaved_changes.ts | 45 ++---------- .../public/react_embeddable_system/types.ts | 24 +----- .../presentation_panel_error.tsx | 7 +- .../embeddable_enhanced/public/plugin.ts | 7 +- 22 files changed, 190 insertions(+), 207 deletions(-) create mode 100644 packages/presentation/presentation_publishing/comparators/index.ts create mode 100644 packages/presentation/presentation_publishing/comparators/state_comparators.ts create mode 100644 packages/presentation/presentation_publishing/comparators/types.ts rename packages/presentation/presentation_publishing/interfaces/{ => titles}/publishes_panel_description.ts (98%) rename packages/presentation/presentation_publishing/interfaces/{ => titles}/publishes_panel_title.test.ts (100%) rename packages/presentation/presentation_publishing/interfaces/{ => titles}/publishes_panel_title.ts (63%) rename src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.test.ts => packages/presentation/presentation_publishing/interfaces/titles/titles_api.test.ts (72%) rename src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.ts => packages/presentation/presentation_publishing/interfaces/titles/titles_api.ts (57%) diff --git a/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx index f1aeac5d04dc..e7f4cc5daedf 100644 --- a/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/eui_markdown/eui_markdown_react_embeddable.tsx @@ -9,12 +9,15 @@ import { EuiMarkdownEditor, EuiMarkdownFormat } from '@elastic/eui'; import { css } from '@emotion/react'; import { - initializeReactEmbeddableTitles, ReactEmbeddableFactory, registerReactEmbeddableFactory, } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { useInheritedViewMode, useStateFromPublishingSubject } from '@kbn/presentation-publishing'; +import { + initializeTitles, + useInheritedViewMode, + useStateFromPublishingSubject, +} from '@kbn/presentation-publishing'; import { euiThemeVars } from '@kbn/ui-theme'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; @@ -40,7 +43,7 @@ const markdownEmbeddableFactory: ReactEmbeddableFactory< /** * initialize state (source of truth) */ - const { titlesApi, titleComparators, serializeTitles } = initializeReactEmbeddableTitles(state); + const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); const content$ = new BehaviorSubject(state.content); /** diff --git a/examples/embeddable_examples/public/react_embeddables/eui_markdown/types.ts b/examples/embeddable_examples/public/react_embeddables/eui_markdown/types.ts index c8fd99d84233..31678aa4a2a3 100644 --- a/examples/embeddable_examples/public/react_embeddables/eui_markdown/types.ts +++ b/examples/embeddable_examples/public/react_embeddables/eui_markdown/types.ts @@ -6,12 +6,10 @@ * Side Public License, v 1. */ -import { - DefaultEmbeddableApi, - SerializedReactEmbeddableTitles, -} from '@kbn/embeddable-plugin/public'; +import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; +import { SerializedTitles } from '@kbn/presentation-publishing'; -export type MarkdownEditorSerializedState = SerializedReactEmbeddableTitles & { +export type MarkdownEditorSerializedState = SerializedTitles & { content: string; }; diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx index ccc40ad7067a..8f7a4955e301 100644 --- a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx @@ -18,13 +18,12 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE, } from '@kbn/data-views-plugin/public'; import { - initializeReactEmbeddableTitles, ReactEmbeddableFactory, registerReactEmbeddableFactory, } from '@kbn/embeddable-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { i18n } from '@kbn/i18n'; -import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { LazyDataViewPicker, withSuspense } from '@kbn/presentation-util-plugin/public'; import { euiThemeVars } from '@kbn/ui-theme'; import { @@ -83,8 +82,7 @@ export const registerFieldListFactory = ( }, buildEmbeddable: async (initialState, buildApi) => { const subscriptions = new Subscription(); - const { titlesApi, titleComparators, serializeTitles } = - initializeReactEmbeddableTitles(initialState); + const { titlesApi, titleComparators, serializeTitles } = initializeTitles(initialState); const allDataViews = await dataViews.getIdsWithTitle(); const selectedDataViewId$ = new BehaviorSubject( diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/types.ts b/examples/embeddable_examples/public/react_embeddables/field_list/types.ts index 9e51d89be58a..347f8683be62 100644 --- a/examples/embeddable_examples/public/react_embeddables/field_list/types.ts +++ b/examples/embeddable_examples/public/react_embeddables/field_list/types.ts @@ -6,12 +6,10 @@ * Side Public License, v 1. */ -import { - DefaultEmbeddableApi, - SerializedReactEmbeddableTitles, -} from '@kbn/embeddable-plugin/public'; +import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; +import { SerializedTitles } from '@kbn/presentation-publishing'; -export type FieldListSerializedStateState = SerializedReactEmbeddableTitles & { +export type FieldListSerializedStateState = SerializedTitles & { dataViewId?: string; selectedFieldNames?: string[]; }; diff --git a/examples/embeddable_examples/public/react_embeddables/search/build_search_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/search/build_search_embeddable.tsx index b84af8ba3b68..2fe28c6e878f 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/build_search_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/search/build_search_embeddable.tsx @@ -12,11 +12,8 @@ import { EuiCallOut } from '@elastic/eui'; import { BehaviorSubject } from 'rxjs'; import { TimeRange } from '@kbn/es-query'; import type { DataView } from '@kbn/data-plugin/common'; -import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import { - EmbeddableStateComparators, - ReactEmbeddableApiRegistration, -} from '@kbn/embeddable-plugin/public/react_embeddable_system/types'; +import { StateComparators, useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import { ReactEmbeddableApiRegistration } from '@kbn/embeddable-plugin/public/react_embeddable_system/types'; import { Api, State, Services } from './types'; import { getCount } from './get_count'; @@ -24,7 +21,7 @@ export const buildSearchEmbeddable = async ( state: State, buildApi: ( apiRegistration: ReactEmbeddableApiRegistration, - comparators: EmbeddableStateComparators + comparators: StateComparators ) => Api, services: Services ) => { diff --git a/packages/presentation/presentation_publishing/comparators/index.ts b/packages/presentation/presentation_publishing/comparators/index.ts new file mode 100644 index 000000000000..788c811805e3 --- /dev/null +++ b/packages/presentation/presentation_publishing/comparators/index.ts @@ -0,0 +1,10 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export type { ComparatorFunction, ComparatorDefinition, StateComparators } from './types'; +export { getInitialValuesFromComparators, runComparators } from './state_comparators'; diff --git a/packages/presentation/presentation_publishing/comparators/state_comparators.ts b/packages/presentation/presentation_publishing/comparators/state_comparators.ts new file mode 100644 index 000000000000..058965890633 --- /dev/null +++ b/packages/presentation/presentation_publishing/comparators/state_comparators.ts @@ -0,0 +1,44 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { StateComparators } from './types'; + +const defaultComparator = (a: T, b: T) => a === b; + +export const getInitialValuesFromComparators = ( + comparators: StateComparators, + comparatorKeys: Array +) => { + const initialValues: Partial = {}; + for (const key of comparatorKeys) { + const comparatorSubject = comparators[key][0]; // 0th element of tuple is the subject + initialValues[key] = comparatorSubject?.value; + } + return initialValues; +}; + +export const runComparators = ( + comparators: StateComparators, + comparatorKeys: Array, + lastSavedState: StateType | undefined, + latestState: Partial +) => { + if (!lastSavedState) { + // if we have no last saved state, everything is considered a change + return latestState; + } + const latestChanges: Partial = {}; + for (const key of comparatorKeys) { + const customComparator = comparators[key]?.[2]; // 2nd element of the tuple is the custom comparator + const comparator = customComparator ?? defaultComparator; + if (!comparator(lastSavedState?.[key], latestState[key], lastSavedState, latestState)) { + latestChanges[key] = latestState[key]; + } + } + return Object.keys(latestChanges).length > 0 ? latestChanges : undefined; +}; diff --git a/packages/presentation/presentation_publishing/comparators/types.ts b/packages/presentation/presentation_publishing/comparators/types.ts new file mode 100644 index 000000000000..dd8bf272452c --- /dev/null +++ b/packages/presentation/presentation_publishing/comparators/types.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { PublishingSubject } from '../publishing_subject'; + +export type ComparatorFunction = ( + last: StateType[KeyType] | undefined, + current: StateType[KeyType] | undefined, + lastState?: Partial, + currentState?: Partial +) => boolean; + +export type ComparatorDefinition = [ + PublishingSubject, + (value: StateType[KeyType]) => void, + ComparatorFunction? +]; + +export type StateComparators = { + [KeyType in keyof StateType]: ComparatorDefinition; +}; diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts index d979669f510a..12a2680d180b 100644 --- a/packages/presentation/presentation_publishing/index.ts +++ b/packages/presentation/presentation_publishing/index.ts @@ -14,6 +14,13 @@ export interface EmbeddableApiContext { embeddable: unknown; } +export { + getInitialValuesFromComparators, + runComparators, + type ComparatorDefinition, + type ComparatorFunction, + type StateComparators, +} from './comparators'; export { apiCanAccessViewMode, getInheritedViewMode, @@ -21,20 +28,20 @@ export { useInheritedViewMode, type CanAccessViewMode, } from './interfaces/can_access_view_mode'; -export { - apiPublishesPhaseEvents, - type PublishesPhaseEvents, - type PhaseEvent, - type PhaseEventType, -} from './interfaces/publishes_phase_events'; +export { apiHasDisableTriggers, type HasDisableTriggers } from './interfaces/has_disable_triggers'; export { hasEditCapabilities, type HasEditCapabilities } from './interfaces/has_edit_capabilities'; export { apiHasParentApi, type HasParentApi } from './interfaces/has_parent_api'; +export { + apiHasSupportedTriggers, + type HasSupportedTriggers, +} from './interfaces/has_supported_triggers'; export { apiHasType, apiIsOfType, type HasType, type HasTypeDisplayName, } from './interfaces/has_type'; +export { apiHasUniqueId, type HasUniqueId } from './interfaces/has_uuid'; export { apiPublishesBlockingError, useBlockingError, @@ -55,6 +62,12 @@ export { useDisabledActionIds, type PublishesDisabledActionIds, } from './interfaces/publishes_disabled_action_ids'; +export { + apiPublishesPhaseEvents, + type PhaseEvent, + type PhaseEventType, + type PublishesPhaseEvents, +} from './interfaces/publishes_phase_events'; export { apiPublishesTimeRange, apiPublishesUnifiedSearch, @@ -64,35 +77,16 @@ export { type PublishesUnifiedSearch, type PublishesWritableUnifiedSearch, } from './interfaces/publishes_unified_search'; -export { - apiPublishesPanelDescription, - apiPublishesWritablePanelDescription, - useDefaultPanelDescription, - usePanelDescription, - type PublishesPanelDescription, - type PublishesWritablePanelDescription, -} from './interfaces/publishes_panel_description'; -export { - getPanelTitle, - apiPublishesPanelTitle, - apiPublishesWritablePanelTitle, - useDefaultPanelTitle, - useHidePanelTitle, - usePanelTitle, - type PublishesPanelTitle, - type PublishesWritablePanelTitle, -} from './interfaces/publishes_panel_title'; export { apiPublishesSavedObjectId, useSavedObjectId, type PublishesSavedObjectId, } from './interfaces/publishes_saved_object_id'; -export { apiHasUniqueId, type HasUniqueId } from './interfaces/has_uuid'; -export { apiHasDisableTriggers, type HasDisableTriggers } from './interfaces/has_disable_triggers'; export { - apiHasSupportedTriggers, - type HasSupportedTriggers, -} from './interfaces/has_supported_triggers'; + apiPublishesUnsavedChanges, + useUnsavedChanges, + type PublishesUnsavedChanges, +} from './interfaces/publishes_unsaved_changes'; export { apiPublishesViewMode, apiPublishesWritableViewMode, @@ -102,13 +96,24 @@ export { type ViewMode, } from './interfaces/publishes_view_mode'; export { - type PublishesUnsavedChanges, - apiPublishesUnsavedChanges, - useUnsavedChanges, -} from './interfaces/publishes_unsaved_changes'; + apiPublishesPanelDescription, + apiPublishesWritablePanelDescription, + useDefaultPanelDescription, + usePanelDescription, + type PublishesPanelDescription, + type PublishesWritablePanelDescription, +} from './interfaces/titles/publishes_panel_description'; +export { + apiPublishesPanelTitle, + apiPublishesWritablePanelTitle, + getPanelTitle, + type PublishesPanelTitle, + type PublishesWritablePanelTitle, +} from './interfaces/titles/publishes_panel_title'; +export { initializeTitles, type SerializedTitles } from './interfaces/titles/titles_api'; export { useBatchedPublishingSubjects, - useStateFromPublishingSubject, usePublishingSubject, + useStateFromPublishingSubject, type PublishingSubject, } from './publishing_subject'; diff --git a/packages/presentation/presentation_publishing/interfaces/publishes_panel_description.ts b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.ts similarity index 98% rename from packages/presentation/presentation_publishing/interfaces/publishes_panel_description.ts rename to packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.ts index fb48b7a6228f..b5dfd530d4d5 100644 --- a/packages/presentation/presentation_publishing/interfaces/publishes_panel_description.ts +++ b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject'; +import { PublishingSubject, useStateFromPublishingSubject } from '../../publishing_subject'; export interface PublishesPanelDescription { panelDescription: PublishingSubject; diff --git a/packages/presentation/presentation_publishing/interfaces/publishes_panel_title.test.ts b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.test.ts similarity index 100% rename from packages/presentation/presentation_publishing/interfaces/publishes_panel_title.test.ts rename to packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.test.ts diff --git a/packages/presentation/presentation_publishing/interfaces/publishes_panel_title.ts b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.ts similarity index 63% rename from packages/presentation/presentation_publishing/interfaces/publishes_panel_title.ts rename to packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.ts index 29d82c410581..56cc8a552682 100644 --- a/packages/presentation/presentation_publishing/interfaces/publishes_panel_title.ts +++ b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject'; +import { PublishingSubject } from '../../publishing_subject'; export interface PublishesPanelTitle { panelTitle: PublishingSubject; @@ -44,24 +44,3 @@ export const apiPublishesWritablePanelTitle = ( typeof (unknownApi as PublishesWritablePanelTitle).setHidePanelTitle === 'function' ); }; - -/** - * A hook that gets this API's panel title as a reactive variable which will cause re-renders on change. - */ -export const usePanelTitle = (api: Partial | undefined) => { - const title = useStateFromPublishingSubject(api?.panelTitle); - const defaultTitle = useStateFromPublishingSubject(api?.defaultPanelTitle); - return title || defaultTitle; -}; - -/** - * A hook that gets this API's hide panel title setting as a reactive variable which will cause re-renders on change. - */ -export const useHidePanelTitle = (api: Partial | undefined) => - useStateFromPublishingSubject(api?.hidePanelTitle); - -/** - * A hook that gets this API's default title as a reactive variable which will cause re-renders on change. - */ -export const useDefaultPanelTitle = (api: Partial | undefined) => - useStateFromPublishingSubject(api?.defaultPanelTitle); diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.test.ts b/packages/presentation/presentation_publishing/interfaces/titles/titles_api.test.ts similarity index 72% rename from src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.test.ts rename to packages/presentation/presentation_publishing/interfaces/titles/titles_api.test.ts index be240cafb7ea..4cb816094a9d 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.test.ts +++ b/packages/presentation/presentation_publishing/interfaces/titles/titles_api.test.ts @@ -6,27 +6,24 @@ * Side Public License, v 1. */ -import { - initializeReactEmbeddableTitles, - SerializedReactEmbeddableTitles, -} from './react_embeddable_titles'; +import { initializeTitles, SerializedTitles } from './titles_api'; -describe('react embeddable titles', () => { - const rawState: SerializedReactEmbeddableTitles = { +describe('titles api', () => { + const rawState: SerializedTitles = { title: 'very cool title', description: 'less cool description', hidePanelTitles: false, }; it('should initialize publishing subjects with the provided rawState', () => { - const { titlesApi } = initializeReactEmbeddableTitles(rawState); + const { titlesApi } = initializeTitles(rawState); expect(titlesApi.panelTitle.value).toBe(rawState.title); expect(titlesApi.panelDescription.value).toBe(rawState.description); expect(titlesApi.hidePanelTitle.value).toBe(rawState.hidePanelTitles); }); it('should update publishing subject values when set functions are called', () => { - const { titlesApi } = initializeReactEmbeddableTitles(rawState); + const { titlesApi } = initializeTitles(rawState); titlesApi.setPanelTitle('even cooler title'); titlesApi.setPanelDescription('super uncool description'); @@ -38,21 +35,21 @@ describe('react embeddable titles', () => { }); it('should correctly serialize current state', () => { - const { serializeTitles, titlesApi } = initializeReactEmbeddableTitles(rawState); + const { serializeTitles, titlesApi } = initializeTitles(rawState); titlesApi.setPanelTitle('UH OH, A TITLE'); const serializedTitles = serializeTitles(); expect(serializedTitles).toMatchInlineSnapshot(` - Object { - "description": "less cool description", - "hidePanelTitles": false, - "title": "UH OH, A TITLE", - } - `); + Object { + "description": "less cool description", + "hidePanelTitles": false, + "title": "UH OH, A TITLE", + } + `); }); it('should return the correct set of comparators', () => { - const { titleComparators } = initializeReactEmbeddableTitles(rawState); + const { titleComparators } = initializeTitles(rawState); expect(titleComparators.title).toBeDefined(); expect(titleComparators.description).toBeDefined(); @@ -60,7 +57,7 @@ describe('react embeddable titles', () => { }); it('should correctly compare hidePanelTitles with custom comparator', () => { - const { titleComparators } = initializeReactEmbeddableTitles(rawState); + const { titleComparators } = initializeTitles(rawState); expect(titleComparators.hidePanelTitles![2]!(true, false)).toBe(false); expect(titleComparators.hidePanelTitles![2]!(undefined, false)).toBe(true); diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.ts b/packages/presentation/presentation_publishing/interfaces/titles/titles_api.ts similarity index 57% rename from src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.ts rename to packages/presentation/presentation_publishing/interfaces/titles/titles_api.ts index 3b65b962694e..026ceb1cc84f 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_titles.ts +++ b/packages/presentation/presentation_publishing/interfaces/titles/titles_api.ts @@ -6,28 +6,25 @@ * Side Public License, v 1. */ -import { - PublishesWritablePanelDescription, - PublishesWritablePanelTitle, -} from '@kbn/presentation-publishing'; import { BehaviorSubject } from 'rxjs'; -import { EmbeddableStateComparators } from './types'; +import { StateComparators } from '../../comparators'; +import { PublishesWritablePanelDescription } from './publishes_panel_description'; +import { PublishesWritablePanelTitle } from './publishes_panel_title'; -export interface SerializedReactEmbeddableTitles { +export interface SerializedTitles { title?: string; description?: string; hidePanelTitles?: boolean; } -export type ReactEmbeddableTitlesApi = PublishesWritablePanelTitle & - PublishesWritablePanelDescription; +export interface TitlesApi extends PublishesWritablePanelTitle, PublishesWritablePanelDescription {} -export const initializeReactEmbeddableTitles = ( - rawState: SerializedReactEmbeddableTitles +export const initializeTitles = ( + rawState: SerializedTitles ): { - titlesApi: ReactEmbeddableTitlesApi; - titleComparators: EmbeddableStateComparators; - serializeTitles: () => SerializedReactEmbeddableTitles; + titlesApi: TitlesApi; + titleComparators: StateComparators; + serializeTitles: () => SerializedTitles; } => { const panelTitle = new BehaviorSubject(rawState.title); const panelDescription = new BehaviorSubject(rawState.description); @@ -37,7 +34,7 @@ export const initializeReactEmbeddableTitles = ( const setHidePanelTitle = (value: boolean | undefined) => hidePanelTitle.next(value); const setPanelDescription = (value: string | undefined) => panelDescription.next(value); - const titleComparators: EmbeddableStateComparators = { + const titleComparators: StateComparators = { title: [panelTitle, setPanelTitle], description: [panelDescription, setPanelDescription], hidePanelTitles: [hidePanelTitle, setHidePanelTitle, (a, b) => Boolean(a) === Boolean(b)], @@ -53,18 +50,12 @@ export const initializeReactEmbeddableTitles = ( }; return { - serializeTitles: () => serializeReactEmbeddableTitles(titlesApi), + serializeTitles: () => ({ + title: panelTitle.value, + hidePanelTitles: hidePanelTitle.value, + description: panelDescription.value, + }), titleComparators, titlesApi, }; }; - -export const serializeReactEmbeddableTitles = ( - titlesApi: ReactEmbeddableTitlesApi -): SerializedReactEmbeddableTitles => { - return { - title: titlesApi.panelTitle.value, - hidePanelTitles: titlesApi.hidePanelTitle.value, - description: titlesApi.panelDescription.value, - }; -}; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 52e408e8ebcd..bf317300732a 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -100,10 +100,6 @@ export { type DefaultEmbeddableApi, type ReactEmbeddableFactory, type ReactEmbeddableRegistration, - type ReactEmbeddableTitlesApi, - type SerializedReactEmbeddableTitles, - initializeReactEmbeddableTitles, - serializeReactEmbeddableTitles, startTrackingEmbeddableUnsavedChanges, } from './react_embeddable_system'; diff --git a/src/plugins/embeddable/public/react_embeddable_system/index.ts b/src/plugins/embeddable/public/react_embeddable_system/index.ts index b48289b7c412..00f77c87cb5b 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/index.ts +++ b/src/plugins/embeddable/public/react_embeddable_system/index.ts @@ -11,12 +11,6 @@ export { registerReactEmbeddableFactory, } from './react_embeddable_registry'; export { ReactEmbeddableRenderer } from './react_embeddable_renderer'; -export { - initializeReactEmbeddableTitles, - serializeReactEmbeddableTitles, - type ReactEmbeddableTitlesApi, - type SerializedReactEmbeddableTitles, -} from './react_embeddable_titles'; export { startTrackingEmbeddableUnsavedChanges } from './react_embeddable_unsaved_changes'; export type { DefaultEmbeddableApi, diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx index 768ffac88eb2..ec5a763bae28 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx +++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx @@ -12,13 +12,13 @@ import { SerializedPanelState, } from '@kbn/presentation-containers'; import { PresentationPanel } from '@kbn/presentation-panel-plugin/public'; +import { StateComparators } from '@kbn/presentation-publishing'; import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import { v4 as generateId } from 'uuid'; import { getReactEmbeddableFactory } from './react_embeddable_registry'; import { startTrackingEmbeddableUnsavedChanges } from './react_embeddable_unsaved_changes'; import { DefaultEmbeddableApi, - EmbeddableStateComparators, ReactEmbeddableApiRegistration, ReactEmbeddableFactory, } from './types'; @@ -56,7 +56,7 @@ export const ReactEmbeddableRenderer = < >; const registerApi = ( apiRegistration: ReactEmbeddableApiRegistration, - comparators: EmbeddableStateComparators + comparators: StateComparators ) => { const { unsavedChanges, resetUnsavedChanges, cleanup } = startTrackingEmbeddableUnsavedChanges( diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.test.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.test.tsx index be3ecb4645fa..68be8ca5c097 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.test.tsx +++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.test.tsx @@ -12,10 +12,10 @@ import { SerializedPanelState, } from '@kbn/presentation-containers'; import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; +import { StateComparators } from '@kbn/presentation-publishing'; import { waitFor } from '@testing-library/react'; import { BehaviorSubject, Subject } from 'rxjs'; import { startTrackingEmbeddableUnsavedChanges } from './react_embeddable_unsaved_changes'; -import { EmbeddableStateComparators } from './types'; interface SuperTestStateType { name: string; @@ -26,7 +26,7 @@ interface SuperTestStateType { describe('react embeddable unsaved changes', () => { let initialState: SuperTestStateType; let lastSavedState: SuperTestStateType; - let comparators: EmbeddableStateComparators; + let comparators: StateComparators; let deserializeState: (state: SerializedPanelState) => SuperTestStateType; let parentApi: (PresentationContainer & PublishesLastSavedState) | null; @@ -47,7 +47,7 @@ describe('react embeddable unsaved changes', () => { const nameSubject = new BehaviorSubject(initialState.name); const ageSubject = new BehaviorSubject(initialState.age); const taglineSubject = new BehaviorSubject(initialState.tagline); - const defaultComparators: EmbeddableStateComparators = { + const defaultComparators: StateComparators = { name: [nameSubject, jest.fn((nextName) => nameSubject.next(nextName))], age: [ageSubject, jest.fn((nextAge) => ageSubject.next(nextAge))], tagline: [taglineSubject, jest.fn((nextTagline) => taglineSubject.next(nextTagline))], @@ -56,7 +56,7 @@ describe('react embeddable unsaved changes', () => { }; const startTrackingUnsavedChanges = ( - customComparators?: EmbeddableStateComparators + customComparators?: StateComparators ) => { comparators = customComparators ?? initializeDefaultComparators(); deserializeState = jest.fn((state) => state.rawState as SuperTestStateType); @@ -142,7 +142,7 @@ describe('react embeddable unsaved changes', () => { lastSavedState.age = 20; initialState.age = 50; const ageSubject = new BehaviorSubject(initialState.age); - const customComparators: EmbeddableStateComparators = { + const customComparators: StateComparators = { ...initializeDefaultComparators(), age: [ ageSubject, diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.ts b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.ts index b717c47737ad..f4bc6bbfc4ad 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.ts +++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_unsaved_changes.ts @@ -11,24 +11,14 @@ import { PresentationContainer, SerializedPanelState, } from '@kbn/presentation-containers'; -import { PublishingSubject } from '@kbn/presentation-publishing'; +import { + getInitialValuesFromComparators, + PublishingSubject, + runComparators, + StateComparators, +} from '@kbn/presentation-publishing'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { combineLatestWith, debounceTime, map } from 'rxjs/operators'; -import { EmbeddableStateComparators } from './types'; - -const defaultComparator = (a: T, b: T) => a === b; - -const getInitialValuesFromComparators = ( - comparators: EmbeddableStateComparators, - comparatorKeys: Array -) => { - const initialValues: Partial = {}; - for (const key of comparatorKeys) { - const comparatorSubject = comparators[key][0]; // 0th element of tuple is the subject - initialValues[key] = comparatorSubject?.value; - } - return initialValues; -}; const getDefaultDiffingApi = () => { return { @@ -38,31 +28,10 @@ const getDefaultDiffingApi = () => { }; }; -const runComparators = ( - comparators: EmbeddableStateComparators, - comparatorKeys: Array, - lastSavedState: StateType | undefined, - latestState: Partial -) => { - if (!lastSavedState) { - // if the parent API provides last saved state, but it's empty for this panel, all of our latest state is unsaved. - return latestState; - } - const latestChanges: Partial = {}; - for (const key of comparatorKeys) { - const customComparator = comparators[key]?.[2]; // 2nd element of the tuple is the custom comparator - const comparator = customComparator ?? defaultComparator; - if (!comparator(lastSavedState?.[key], latestState[key], lastSavedState, latestState)) { - latestChanges[key] = latestState[key]; - } - } - return Object.keys(latestChanges).length > 0 ? latestChanges : undefined; -}; - export const startTrackingEmbeddableUnsavedChanges = ( uuid: string, parentApi: PresentationContainer | undefined, - comparators: EmbeddableStateComparators, + comparators: StateComparators, deserializeState: (state: SerializedPanelState) => StateType ) => { if (Object.keys(comparators).length === 0) return getDefaultDiffingApi(); diff --git a/src/plugins/embeddable/public/react_embeddable_system/types.ts b/src/plugins/embeddable/public/react_embeddable_system/types.ts index 5068c0c16fdf..6c54b13ce5c5 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/types.ts +++ b/src/plugins/embeddable/public/react_embeddable_system/types.ts @@ -11,7 +11,7 @@ import { SerializedPanelState, } from '@kbn/presentation-containers'; import { DefaultPresentationPanelApi } from '@kbn/presentation-panel-plugin/public/panel_component/types'; -import { HasType, PublishesUnsavedChanges, PublishingSubject } from '@kbn/presentation-publishing'; +import { HasType, PublishesUnsavedChanges, StateComparators } from '@kbn/presentation-publishing'; import React, { ReactElement } from 'react'; export type ReactEmbeddableRegistration< @@ -45,29 +45,9 @@ export interface ReactEmbeddableFactory< initialState: StateType, buildApi: ( apiRegistration: ReactEmbeddableApiRegistration, - comparators: EmbeddableStateComparators + comparators: StateComparators ) => ApiType, uuid: string, parentApi?: PresentationContainer ) => Promise<{ Component: React.FC<{}>; api: ApiType }>; } - -/** - * State comparators - */ -export type EmbeddableComparatorFunction = ( - last: StateType[KeyType] | undefined, - current: StateType[KeyType] | undefined, - lastState?: Partial, - currentState?: Partial -) => boolean; - -export type EmbeddableComparatorDefinition = [ - PublishingSubject, - (value: StateType[KeyType]) => void, - EmbeddableComparatorFunction? -]; - -export type EmbeddableStateComparators = { - [KeyType in keyof StateType]: EmbeddableComparatorDefinition; -}; diff --git a/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx b/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx index 92f7a0045003..5b7de4ec05cc 100644 --- a/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx +++ b/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx @@ -10,10 +10,9 @@ import { EuiButtonEmpty, EuiEmptyPrompt, EuiText } from '@elastic/eui'; import React, { useEffect, useMemo, useState } from 'react'; import { ErrorLike } from '@kbn/expressions-plugin/common'; -import { Markdown } from '@kbn/shared-ux-markdown'; +import { useStateFromPublishingSubject } from '@kbn/presentation-publishing'; import { renderSearchError } from '@kbn/search-errors'; - -import { usePanelTitle } from '@kbn/presentation-publishing'; +import { Markdown } from '@kbn/shared-ux-markdown'; import { Subscription } from 'rxjs'; import { editPanelAction } from '../panel_actions/panel_actions'; import { getErrorCallToAction } from './presentation_panel_strings'; @@ -36,7 +35,7 @@ export const PresentationPanelError = ({ [api, isEditable] ); - const panelTitle = usePanelTitle(api); + const panelTitle = useStateFromPublishingSubject(api?.panelTitle); const ariaLabel = useMemo( () => (panelTitle ? getErrorCallToAction(panelTitle) : label), [label, panelTitle] diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index ac52d1f78974..58e6a04ee661 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -18,8 +18,7 @@ import { IEmbeddable, PANEL_NOTIFICATION_TRIGGER, } from '@kbn/embeddable-plugin/public'; -import { EmbeddableStateComparators } from '@kbn/embeddable-plugin/public/react_embeddable_system/types'; -import { apiHasUniqueId } from '@kbn/presentation-publishing'; +import { apiHasUniqueId, StateComparators } from '@kbn/presentation-publishing'; import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common'; import { AdvancedUiActionsSetup, @@ -57,7 +56,7 @@ export interface StartContract { state: DynamicActionsSerializedState ) => { dynamicActionsApi: HasDynamicActions; - dynamicActionsComparator: EmbeddableStateComparators; + dynamicActionsComparator: StateComparators; serializeDynamicActions: () => DynamicActionsSerializedState; }; } @@ -137,7 +136,7 @@ export class EmbeddableEnhancedPlugin state: DynamicActionsSerializedState ): { dynamicActionsApi: HasDynamicActions; - dynamicActionsComparator: EmbeddableStateComparators; + dynamicActionsComparator: StateComparators; serializeDynamicActions: () => DynamicActionsSerializedState; } { const dynamicActionsState$ = new BehaviorSubject(