Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Embeddables rebuild] Add new registry #176018

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/embeddable_examples/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
FilterDebuggerEmbeddableFactory,
FilterDebuggerEmbeddableFactoryDefinition,
} from './filter_debugger';
import { registerMarkdownEditorEmbeddable } from './react_embeddables/eui_markdown_react_embeddable';

export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
Expand All @@ -53,6 +54,8 @@ export interface EmbeddableExamplesStart {
factories: ExampleEmbeddableFactories;
}

registerMarkdownEditorEmbeddable();

export class EmbeddableExamplesPlugin
implements
Plugin<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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 { EuiMarkdownEditor, EuiMarkdownFormat } from '@elastic/eui';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import {
ReactEmbeddableFactory,
RegisterReactEmbeddable,
registerReactEmbeddableFactory,
useReactEmbeddableApiHandle,
initializeReactEmbeddableUuid,
initializeReactEmbeddableTitles,
SerializedReactEmbeddableTitles,
DefaultEmbeddableApi,
useReactEmbeddableUnsavedChanges,
} from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { useInheritedViewMode, useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import React from 'react';
import { BehaviorSubject } from 'rxjs';

// -----------------------------------------------------------------------------
// Types for this embeddable
// -----------------------------------------------------------------------------
type MarkdownEditorSerializedState = SerializedReactEmbeddableTitles & {
content: string;
};

type MarkdownEditorApi = DefaultEmbeddableApi;

const type = 'euiMarkdown';

// -----------------------------------------------------------------------------
// Define the Embeddable Factory
// -----------------------------------------------------------------------------
const markdownEmbeddableFactory: ReactEmbeddableFactory<
MarkdownEditorSerializedState,
MarkdownEditorApi
> = {
// -----------------------------------------------------------------------------
// Deserialize function
// -----------------------------------------------------------------------------
deserializeState: (state) => {
// We could run migrations here.
// We should inject references here. References are given as state.references

return state.rawState as MarkdownEditorSerializedState;
},

// -----------------------------------------------------------------------------
// Register the Embeddable component
// -----------------------------------------------------------------------------
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);

/**
* 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.
*/

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
);

// get state for rendering
const content = useStateFromPublishingSubject(contentSubject);
const viewMode = useInheritedViewMode(thisApi) ?? 'view';

return viewMode === 'edit' ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about lazy loading component. Examples should show best practices and best practices recommend keeping bundle size as small as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. This markdown example is mostly there for testing purposes at the moment. I've created this follow-up issue to describe further examples that should be built, and an async-imported embeddable is high up on the list.

<EuiMarkdownEditor
css={css`
width: 100%;
`}
value={content ?? ''}
onChange={(value) => contentSubject.next(value)}
aria-label={i18n.translate('dashboard.test.markdownEditor.ariaLabel', {
defaultMessage: 'Dashboard markdown editor',
})}
height="full"
/>
) : (
<EuiMarkdownFormat
css={css`
padding: ${euiThemeVars.euiSizeS};
`}
>
{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
// -----------------------------------------------------------------------------
export const registerMarkdownEditorEmbeddable = () =>
registerReactEmbeddableFactory(type, markdownEmbeddableFactory);
8 changes: 4 additions & 4 deletions examples/embeddable_examples/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
"server/**/*.ts",
"../../typings/**/*"
],
"exclude": [
"target/**/*",
],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/kibana-utils-plugin",
"@kbn/ui-actions-plugin",
"@kbn/embeddable-plugin",
"@kbn/presentation-publishing",
"@kbn/ui-theme",
"@kbn/i18n",
"@kbn/es-query",
"@kbn/es-query"
]
}
6 changes: 6 additions & 0 deletions packages/presentation/presentation_containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ export {
type PresentationContainer,
} from './interfaces/presentation_container';
export { tracksOverlays, type TracksOverlays } from './interfaces/tracks_overlays';
export { type SerializedPanelState } from './interfaces/serialized_state';
export {
type PublishesLastSavedState,
apiPublishesLastSavedState,
getLastSavedStateSubjectForChild,
} from './interfaces/last_saved_state';
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 '@kbn/presentation-publishing';
import { BehaviorSubject, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { SerializedPanelState } from './serialized_state';

export interface PublishesLastSavedState {
lastSavedState: Subject<void>; // a notification that the last saved state has changed
getLastSavedStateForChild: (childId: string) => SerializedPanelState | undefined;
}

export const apiPublishesLastSavedState = (api: unknown): api is PublishesLastSavedState => {
return Boolean(
api &&
(api as PublishesLastSavedState).lastSavedState &&
(api as PublishesLastSavedState).getLastSavedStateForChild
);
};

export const getLastSavedStateSubjectForChild = <StateType extends unknown = unknown>(
parentApi: unknown,
childId: string,
deserializer?: (state: SerializedPanelState) => StateType
): PublishingSubject<StateType | undefined> | undefined => {
if (!parentApi) return;
const fetchUnsavedChanges = (): StateType | undefined => {
if (!apiPublishesLastSavedState(parentApi)) return;
const rawLastSavedState = parentApi.getLastSavedStateForChild(childId);
if (rawLastSavedState === undefined) return;
return deserializer
? deserializer(rawLastSavedState)
: (rawLastSavedState.rawState as StateType);
};

const lastSavedStateForChild = new BehaviorSubject<StateType | undefined>(fetchUnsavedChanges());
if (!apiPublishesLastSavedState(parentApi)) return;
parentApi.lastSavedState
.pipe(
map(() => fetchUnsavedChanges()),
filter((rawLastSavedState) => rawLastSavedState !== undefined)
)
.subscribe(lastSavedStateForChild);
return lastSavedStateForChild;
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,4 @@ export const apiCanExpandPanels = (unknownApi: unknown | null): unknownApi is Ca
* Gets this API's expanded panel state as a reactive variable which will cause re-renders on change.
*/
export const useExpandedPanelId = (api: Partial<CanExpandPanels> | undefined) =>
useStateFromPublishingSubject<string | undefined, CanExpandPanels['expandedPanelId']>(
apiCanExpandPanels(api) ? api.expandedPanelId : undefined
);
useStateFromPublishingSubject(apiCanExpandPanels(api) ? api.expandedPanelId : undefined);
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,32 @@
*/

import { apiHasParentApi, PublishesViewMode } from '@kbn/presentation-publishing';
import { PublishesLastSavedState } from './last_saved_state';

export interface PanelPackage {
panelType: string;
initialState: unknown;
}
export interface PresentationContainer extends Partial<PublishesViewMode> {
removePanel: (panelId: string) => void;
canRemovePanels?: () => boolean;
replacePanel: (idToRemove: string, newPanel: PanelPackage) => Promise<string>;
}

export type PresentationContainer = Partial<PublishesViewMode> &
PublishesLastSavedState & {
registerPanelApi: <ApiType extends unknown = unknown>(
panelId: string,
panelApi: ApiType
) => void;
removePanel: (panelId: string) => void;
canRemovePanels?: () => boolean;
replacePanel: (idToRemove: string, newPanel: PanelPackage) => Promise<string>;
};

export const apiIsPresentationContainer = (
unknownApi: unknown | null
): unknownApi is PresentationContainer => {
return Boolean((unknownApi as PresentationContainer)?.removePanel !== undefined);
return Boolean(
(unknownApi as PresentationContainer)?.removePanel !== undefined &&
(unknownApi as PresentationContainer)?.registerPanelApi !== undefined &&
(unknownApi as PresentationContainer)?.replacePanel !== undefined
);
};

export const getContainerParentFromAPI = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 type { SavedObjectReference } from '@kbn/core-saved-objects-api-server';

/**
* A package containing the serialized Embeddable state, with references extracted. When saving Embeddables using any
* strategy, this is the format that should be used.
*/
export interface SerializedPanelState<RawStateType extends object = object> {
references?: SavedObjectReference[];
rawState: RawStateType;
version?: string;
}
20 changes: 20 additions & 0 deletions packages/presentation/presentation_containers/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 { Subject } from 'rxjs';
import { PresentationContainer } from './interfaces/presentation_container';

export const getMockPresentationContainer = (): PresentationContainer => {
return {
registerPanelApi: jest.fn(),
removePanel: jest.fn(),
replacePanel: jest.fn(),
lastSavedState: new Subject<void>(),
getLastSavedStateForChild: jest.fn(),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["target/**/*"],
"kbn_references": ["@kbn/presentation-publishing", "@kbn/core-mount-utils-browser"]
"kbn_references": [
"@kbn/presentation-publishing",
"@kbn/core-mount-utils-browser",
"@kbn/core-saved-objects-api-server",
]
}
5 changes: 5 additions & 0 deletions packages/presentation/presentation_publishing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ export {
type PublishesWritableViewMode,
type ViewMode,
} from './interfaces/publishes_view_mode';
export {
type PublishesUnsavedChanges,
apiPublishesUnsavedChanges,
useUnsavedChanges,
} from './interfaces/publishes_unsaved_changes';
export {
useBatchedPublishingSubjects,
useStateFromPublishingSubject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { useStateFromPublishingSubject } from '../publishing_subject';
import { apiHasParentApi, HasParentApi } from './has_parent_api';
import { apiPublishesViewMode, PublishesViewMode, ViewMode } from './publishes_view_mode';
import { apiPublishesViewMode, PublishesViewMode } from './publishes_view_mode';

/**
* This API can access a view mode, either its own or from its parent API.
Expand Down Expand Up @@ -49,6 +49,5 @@ export const getViewModeSubject = (api?: CanAccessViewMode) => {
export const useInheritedViewMode = <ApiType extends CanAccessViewMode = CanAccessViewMode>(
api: ApiType | undefined
) => {
const subject = getViewModeSubject(api);
useStateFromPublishingSubject<ViewMode, typeof subject>(subject);
return useStateFromPublishingSubject(getViewModeSubject(api));
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@ export const apiPublishesBlockingError = (
* Gets this API's fatal error as a reactive variable which will cause re-renders on change.
*/
export const useBlockingError = (api: Partial<PublishesBlockingError> | undefined) =>
useStateFromPublishingSubject<Error | undefined, PublishesBlockingError['blockingError']>(
api?.blockingError
);
useStateFromPublishingSubject(api?.blockingError);
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@ export const apiPublishesDataLoading = (
* Gets this API's data loading state as a reactive variable which will cause re-renders on change.
*/
export const useDataLoading = (api: Partial<PublishesDataLoading> | undefined) =>
useStateFromPublishingSubject<boolean | undefined, PublishesDataLoading['dataLoading']>(
apiPublishesDataLoading(api) ? api.dataLoading : undefined
);
useStateFromPublishingSubject(apiPublishesDataLoading(api) ? api.dataLoading : undefined);
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,4 @@ export const apiPublishesDataViews = (
* Gets this API's data views as a reactive variable which will cause re-renders on change.
*/
export const useDataViews = (api: Partial<PublishesDataViews> | undefined) =>
useStateFromPublishingSubject<DataView[] | undefined, PublishesDataViews['dataViews']>(
apiPublishesDataViews(api) ? api.dataViews : undefined
);
useStateFromPublishingSubject(apiPublishesDataViews(api) ? api.dataViews : undefined);
Loading