Skip to content

Commit

Permalink
[Embeddable Rebuild] Change Parenting Structure (#178426)
Browse files Browse the repository at this point in the history
Changes the structure of the React Embeddable system's factories to reduce boilerplate and potential for bugs.
  • Loading branch information
ThomThomson authored Mar 13, 2024
1 parent c312bae commit fa799d7
Show file tree
Hide file tree
Showing 22 changed files with 530 additions and 595 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@ import { EuiMarkdownEditor, EuiMarkdownFormat } from '@elastic/eui';
import { css } from '@emotion/react';
import {
initializeReactEmbeddableTitles,
initializeReactEmbeddableUuid,
ReactEmbeddableFactory,
RegisterReactEmbeddable,
registerReactEmbeddableFactory,
useReactEmbeddableApiHandle,
useReactEmbeddableUnsavedChanges,
} from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { useInheritedViewMode, useStateFromPublishingSubject } from '@kbn/presentation-publishing';
Expand All @@ -25,78 +21,72 @@ import { BehaviorSubject } from 'rxjs';
import { EUI_MARKDOWN_ID } from './constants';
import { MarkdownEditorSerializedState, MarkdownEditorApi } from './types';

export const registerMarkdownEditorEmbeddable = () => {
const markdownEmbeddableFactory: ReactEmbeddableFactory<
MarkdownEditorSerializedState,
MarkdownEditorApi
> = {
deserializeState: (state) => {
/**
* Here we can run migrations and inject references.
*/
return state.rawState as MarkdownEditorSerializedState;
},
getComponent: async (state, maybeId) => {
/**
* initialize state (source of truth)
*/
const uuid = initializeReactEmbeddableUuid(maybeId);
const { titlesApi, titleComparators, serializeTitles } =
initializeReactEmbeddableTitles(state);
const contentSubject = new BehaviorSubject(state.content);
const markdownEmbeddableFactory: ReactEmbeddableFactory<
MarkdownEditorSerializedState,
MarkdownEditorApi
> = {
type: EUI_MARKDOWN_ID,
deserializeState: (state) => {
/**
* Here we can run clientside migrations and inject references.
*/
return state.rawState as MarkdownEditorSerializedState;
},
/**
* The buildEmbeddable function is async so you can async import the component or load a saved
* object here. The loading will be handed gracefully by the Presentation Container.
*/
buildEmbeddable: async (state, buildApi) => {
/**
* initialize state (source of truth)
*/
const { titlesApi, titleComparators, serializeTitles } = initializeReactEmbeddableTitles(state);
const content$ = new BehaviorSubject(state.content);

/**
* Register the API for this embeddable. This API will be published into the imperative handle
* of the React component. Methods on this API will be exposed to siblings, to registered actions
* and to the parent api.
*/
const api = buildApi(
{
...titlesApi,
serializeState: () => {
return {
rawState: {
...serializeTitles(),
content: content$.getValue(),
},
};
},
},

/**
* getComponent is async so you can async import the component or load a saved object here.
* the loading will be handed gracefully by the Presentation Container.
* Provide state comparators. Each comparator is 3 element tuple:
* 1) current value (publishing subject)
* 2) setter, allowing parent to reset value
* 3) optional comparator which provides logic to diff lasted stored value and current value
*/
{
content: [content$, (value) => content$.next(value)],
...titleComparators,
}
);

return RegisterReactEmbeddable((apiRef) => {
/**
* Unsaved changes logic is handled automatically by this hook. You only need to provide
* a subject, setter, and optional state comparator for each key in your state type.
*/
const { unsavedChanges, resetUnsavedChanges } = useReactEmbeddableUnsavedChanges(
uuid,
markdownEmbeddableFactory,
{
content: [contentSubject, (value) => contentSubject.next(value)],
...titleComparators,
}
);

/**
* Publish the API. This is what gets forwarded to the Actions framework, and to whatever the
* parent of this embeddable is.
*/
const thisApi = useReactEmbeddableApiHandle(
{
...titlesApi,
unsavedChanges,
resetUnsavedChanges,
serializeState: async () => {
return {
rawState: {
...serializeTitles(),
content: contentSubject.getValue(),
},
};
},
},
apiRef,
uuid
);

return {
api,
Component: () => {
// get state for rendering
const content = useStateFromPublishingSubject(contentSubject);
const viewMode = useInheritedViewMode(thisApi) ?? 'view';
const content = useStateFromPublishingSubject(content$);
const viewMode = useInheritedViewMode(api) ?? 'view';

return viewMode === 'edit' ? (
<EuiMarkdownEditor
css={css`
width: 100%;
`}
value={content ?? ''}
onChange={(value) => contentSubject.next(value)}
onChange={(value) => content$.next(value)}
aria-label={i18n.translate('embeddableExamples.euiMarkdownEditor.ariaLabel', {
defaultMessage: 'Dashboard markdown editor',
})}
Expand All @@ -105,20 +95,21 @@ export const registerMarkdownEditorEmbeddable = () => {
) : (
<EuiMarkdownFormat
css={css`
padding: ${euiThemeVars.euiSizeS};
padding: ${euiThemeVars.euiSizeM};
`}
>
{content ?? ''}
</EuiMarkdownFormat>
);
});
},
};

/**
* Register the defined Embeddable Factory - notice that this isn't defined
* on the plugin. Instead, it's a simple imported function. I.E to register an
* embeddable, you only need the embeddable plugin in your requiredBundles
*/
registerReactEmbeddableFactory(EUI_MARKDOWN_ID, markdownEmbeddableFactory);
},
};
},
};

/**
* Register the defined Embeddable Factory - notice that this isn't defined
* on the plugin. Instead, it's a simple imported function. I.E to register an
* embeddable, you only need the embeddable plugin in your requiredBundles
*/
export const registerMarkdownEditorEmbeddable = () =>
registerReactEmbeddableFactory(markdownEmbeddableFactory);
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ export type MarkdownEditorSerializedState = SerializedReactEmbeddableTitles & {
content: string;
};

export type MarkdownEditorApi = DefaultEmbeddableApi;
export type MarkdownEditorApi = DefaultEmbeddableApi<MarkdownEditorSerializedState>;
Loading

0 comments on commit fa799d7

Please sign in to comment.