[Embeddables Rebuild] Async factories (#179302)

Changes the new Embeddable factory definition to take an async function instead of the factory object directly.
This commit is contained in:
Devon Thomson 2024-03-25 15:44:57 -04:00 committed by GitHub
parent db941ae382
commit b4e0a04af9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 272 additions and 261 deletions

View file

@ -7,7 +7,11 @@
*/
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import {
EmbeddableSetup,
EmbeddableStart,
registerReactEmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
@ -35,12 +39,12 @@ import {
FilterDebuggerEmbeddableFactory,
FilterDebuggerEmbeddableFactoryDefinition,
} from './filter_debugger';
import { registerMarkdownEditorEmbeddable } from './react_embeddables/eui_markdown/eui_markdown_react_embeddable';
import { registerCreateEuiMarkdownAction } from './react_embeddables/eui_markdown/create_eui_markdown_action';
import { registerFieldListFactory } from './react_embeddables/field_list/field_list_react_embeddable';
import { registerCreateFieldListAction } from './react_embeddables/field_list/create_field_list_action';
import { registerSearchEmbeddableFactory } from './react_embeddables/search/register_search_embeddable_factory';
import { registerAddSearchPanelAction } from './react_embeddables/search/register_add_search_panel_action';
import { EUI_MARKDOWN_ID } from './react_embeddables/eui_markdown/constants';
import { FIELD_LIST_ID } from './react_embeddables/field_list/constants';
import { SEARCH_EMBEDDABLE_ID } from './react_embeddables/search/constants';
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
@ -114,17 +118,29 @@ export class EmbeddableExamplesPlugin
core: CoreStart,
deps: EmbeddableExamplesStartDependencies
): EmbeddableExamplesStart {
registerFieldListFactory(core, deps);
registerCreateFieldListAction(deps.uiActions);
registerMarkdownEditorEmbeddable();
registerCreateEuiMarkdownAction(deps.uiActions);
registerSearchEmbeddableFactory({
data: deps.data,
dataViews: deps.dataViews,
registerReactEmbeddableFactory(FIELD_LIST_ID, async () => {
const { getFieldListFactory } = await import(
'./react_embeddables/field_list/field_list_react_embeddable'
);
return getFieldListFactory(core, deps);
});
registerCreateEuiMarkdownAction(deps.uiActions);
registerReactEmbeddableFactory(EUI_MARKDOWN_ID, async () => {
const { markdownEmbeddableFactory } = await import(
'./react_embeddables/eui_markdown/eui_markdown_react_embeddable'
);
return markdownEmbeddableFactory;
});
registerAddSearchPanelAction(deps.uiActions);
registerReactEmbeddableFactory(SEARCH_EMBEDDABLE_ID, async () => {
const { getSearchEmbeddableFactory } = await import(
'./react_embeddables/search/search_react_embeddable'
);
return getSearchEmbeddableFactory(deps);
});
return {
createSampleData: async () => {},

View file

@ -8,10 +8,7 @@
import { EuiMarkdownEditor, EuiMarkdownFormat } from '@elastic/eui';
import { css } from '@emotion/react';
import {
ReactEmbeddableFactory,
registerReactEmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import {
initializeTitles,
@ -24,7 +21,7 @@ import { BehaviorSubject } from 'rxjs';
import { EUI_MARKDOWN_ID } from './constants';
import { MarkdownEditorSerializedState, MarkdownEditorApi } from './types';
const markdownEmbeddableFactory: ReactEmbeddableFactory<
export const markdownEmbeddableFactory: ReactEmbeddableFactory<
MarkdownEditorSerializedState,
MarkdownEditorApi
> = {
@ -108,11 +105,3 @@ const markdownEmbeddableFactory: ReactEmbeddableFactory<
};
},
};
/**
* 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);

View file

@ -17,10 +17,7 @@ import {
DataViewsPublicPluginStart,
DATA_VIEW_SAVED_OBJECT_TYPE,
} from '@kbn/data-views-plugin/public';
import {
ReactEmbeddableFactory,
registerReactEmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { i18n } from '@kbn/i18n';
import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
@ -49,7 +46,7 @@ const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOpti
};
};
export const registerFieldListFactory = (
export const getFieldListFactory = (
core: CoreStart,
{
dataViews,
@ -224,6 +221,5 @@ export const registerFieldListFactory = (
};
},
};
registerReactEmbeddableFactory(fieldListEmbeddableFactory);
return fieldListEmbeddableFactory;
};

View file

@ -1,142 +0,0 @@
/*
* 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 React, { useEffect, useState } from 'react';
import fastIsEqual from 'fast-deep-equal';
import { EuiCallOut } from '@elastic/eui';
import { BehaviorSubject } from 'rxjs';
import { TimeRange } from '@kbn/es-query';
import type { DataView } from '@kbn/data-plugin/common';
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';
export const buildSearchEmbeddable = async (
state: State,
buildApi: (
apiRegistration: ReactEmbeddableApiRegistration<State, Api>,
comparators: StateComparators<State>
) => Api,
services: Services
) => {
const defaultDataView = await services.dataViews.getDefaultDataView();
const timeRange$ = new BehaviorSubject<TimeRange | undefined>(state.timeRange);
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(
defaultDataView ? [defaultDataView] : undefined
);
const dataLoading$ = new BehaviorSubject<boolean | undefined>(false);
function setTimeRange(nextTimeRange: TimeRange | undefined) {
timeRange$.next(nextTimeRange);
}
const api = buildApi(
{
dataViews: dataViews$,
timeRange$,
setTimeRange,
dataLoading: dataLoading$,
serializeState: () => {
return {
rawState: {
timeRange: timeRange$.value,
},
references: [],
};
},
},
{
timeRange: [timeRange$, setTimeRange, fastIsEqual],
}
);
const appliedTimeRange$ = new BehaviorSubject(
timeRange$.value ?? api.parentApi?.timeRange$?.value
);
const subscriptions = api.timeRange$.subscribe((timeRange) => {
appliedTimeRange$.next(timeRange ?? api.parentApi?.timeRange$?.value);
});
if (api.parentApi?.timeRange$) {
subscriptions.add(
api.parentApi?.timeRange$.subscribe((parentTimeRange) => {
if (timeRange$?.value) {
return;
}
appliedTimeRange$.next(parentTimeRange);
})
);
}
return {
api,
Component: () => {
const [count, setCount] = useState<number>(0);
const [error, setError] = useState<Error | undefined>();
const [filters, query, appliedTimeRange] = useBatchedPublishingSubjects(
api.parentApi?.filters$,
api.parentApi?.query$,
appliedTimeRange$
);
useEffect(() => {
return () => {
subscriptions.unsubscribe();
};
}, []);
useEffect(() => {
let ignore = false;
setError(undefined);
if (!defaultDataView) {
return;
}
dataLoading$.next(true);
getCount(defaultDataView, services.data, filters ?? [], query, appliedTimeRange)
.then((nextCount: number) => {
if (ignore) {
return;
}
dataLoading$.next(false);
setCount(nextCount);
})
.catch((err) => {
if (ignore) {
return;
}
dataLoading$.next(false);
setError(err);
});
return () => {
ignore = true;
};
}, [filters, query, appliedTimeRange]);
if (!defaultDataView) {
return (
<EuiCallOut title="Default data view not found" color="warning" iconType="warning">
<p>Please install a sample data set to run example.</p>
</EuiCallOut>
);
}
if (error) {
return (
<EuiCallOut title="Search error" color="warning" iconType="warning">
<p>{error.message}</p>
</EuiCallOut>
);
}
return (
<p>
Found <strong>{count}</strong> from {defaultDataView.name}
</p>
);
},
};
};

View file

@ -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 const SEARCH_EMBEDDABLE_ID = 'searchEmbeddableDemo';
export const ADD_SEARCH_ACTION_ID = 'create_search_demo';

View file

@ -9,10 +9,11 @@
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ADD_SEARCH_ACTION_ID, SEARCH_EMBEDDABLE_ID } from './constants';
export const registerAddSearchPanelAction = (uiActions: UiActionsStart) => {
uiActions.registerAction<EmbeddableApiContext>({
id: 'CREATE_SEARCH_REACT_EMBEDDABLE',
id: ADD_SEARCH_ACTION_ID,
getDisplayName: () => 'Unified search example',
getDisplayNameTooltip: () =>
'Demonstrates how to use global filters, global time range, panel time range, and global query state in an embeddable',
@ -24,12 +25,12 @@ export const registerAddSearchPanelAction = (uiActions: UiActionsStart) => {
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
embeddable.addNewPanel(
{
panelType: 'SEARCH_REACT_EMBEDDABLE',
panelType: SEARCH_EMBEDDABLE_ID,
initialState: {},
},
true
);
},
});
uiActions.attachAction('ADD_PANEL_TRIGGER', 'CREATE_SEARCH_REACT_EMBEDDABLE');
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_SEARCH_ACTION_ID);
};

View file

@ -1,28 +0,0 @@
/*
* 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 {
ReactEmbeddableFactory,
registerReactEmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { Api, Services, State } from './types';
export const registerSearchEmbeddableFactory = (services: Services) => {
const factory: ReactEmbeddableFactory<State, Api> = {
type: 'SEARCH_REACT_EMBEDDABLE',
deserializeState: (state) => {
return state.rawState as State;
},
buildEmbeddable: async (state, buildApi) => {
const { buildSearchEmbeddable } = await import('./build_search_embeddable');
return buildSearchEmbeddable(state, buildApi, services);
},
};
registerReactEmbeddableFactory(factory);
};

View file

@ -0,0 +1,145 @@
/*
* 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 { EuiCallOut } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/common';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { TimeRange } from '@kbn/es-query';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import fastIsEqual from 'fast-deep-equal';
import React, { useEffect, useState } from 'react';
import { BehaviorSubject } from 'rxjs';
import { SEARCH_EMBEDDABLE_ID } from './constants';
import { getCount } from './get_count';
import { Api, Services, State } from './types';
export const getSearchEmbeddableFactory = (services: Services) => {
const factory: ReactEmbeddableFactory<State, Api> = {
type: SEARCH_EMBEDDABLE_ID,
deserializeState: (state) => {
return state.rawState as State;
},
buildEmbeddable: async (state, buildApi) => {
const defaultDataView = await services.dataViews.getDefaultDataView();
const timeRange$ = new BehaviorSubject<TimeRange | undefined>(state.timeRange);
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(
defaultDataView ? [defaultDataView] : undefined
);
const dataLoading$ = new BehaviorSubject<boolean | undefined>(false);
function setTimeRange(nextTimeRange: TimeRange | undefined) {
timeRange$.next(nextTimeRange);
}
const api = buildApi(
{
dataViews: dataViews$,
timeRange$,
setTimeRange,
dataLoading: dataLoading$,
serializeState: () => {
return {
rawState: {
timeRange: timeRange$.value,
},
references: [],
};
},
},
{
timeRange: [timeRange$, setTimeRange, fastIsEqual],
}
);
const appliedTimeRange$ = new BehaviorSubject(
timeRange$.value ?? api.parentApi?.timeRange$?.value
);
const subscriptions = api.timeRange$.subscribe((timeRange) => {
appliedTimeRange$.next(timeRange ?? api.parentApi?.timeRange$?.value);
});
if (api.parentApi?.timeRange$) {
subscriptions.add(
api.parentApi?.timeRange$.subscribe((parentTimeRange) => {
if (timeRange$?.value) {
return;
}
appliedTimeRange$.next(parentTimeRange);
})
);
}
return {
api,
Component: () => {
const [count, setCount] = useState<number>(0);
const [error, setError] = useState<Error | undefined>();
const [filters, query, appliedTimeRange] = useBatchedPublishingSubjects(
api.parentApi?.filters$,
api.parentApi?.query$,
appliedTimeRange$
);
useEffect(() => {
return () => {
subscriptions.unsubscribe();
};
}, []);
useEffect(() => {
let ignore = false;
setError(undefined);
if (!defaultDataView) {
return;
}
dataLoading$.next(true);
getCount(defaultDataView, services.data, filters ?? [], query, appliedTimeRange)
.then((nextCount: number) => {
if (ignore) {
return;
}
dataLoading$.next(false);
setCount(nextCount);
})
.catch((err) => {
if (ignore) {
return;
}
dataLoading$.next(false);
setError(err);
});
return () => {
ignore = true;
};
}, [filters, query, appliedTimeRange]);
if (!defaultDataView) {
return (
<EuiCallOut title="Default data view not found" color="warning" iconType="warning">
<p>Please install a sample data set to run example.</p>
</EuiCallOut>
);
}
if (error) {
return (
<EuiCallOut title="Search error" color="warning" iconType="warning">
<p>{error.message}</p>
</EuiCallOut>
);
}
return (
<p>
Found <strong>{count}</strong> from {defaultDataView.name}
</p>
);
},
};
},
};
return factory;
};

View file

@ -14,21 +14,23 @@ import {
import { ReactEmbeddableFactory } from './types';
describe('react embeddable registry', () => {
const testEmbeddableFactory: ReactEmbeddableFactory = {
type: 'test',
deserializeState: jest.fn(),
buildEmbeddable: jest.fn(),
};
const getTestEmbeddableFactory = () =>
Promise.resolve({
type: 'test',
deserializeState: jest.fn(),
buildEmbeddable: jest.fn(),
} as ReactEmbeddableFactory);
it('throws an error if requested embeddable factory type is not registered', () => {
expect(() => getReactEmbeddableFactory('notRegistered')).toThrowErrorMatchingInlineSnapshot(
`"No embeddable factory found for type: notRegistered"`
expect(() => getReactEmbeddableFactory('notRegistered')).rejects.toThrow(
'No embeddable factory found for type: notRegistered'
);
});
it('can register and get an embeddable factory', () => {
registerReactEmbeddableFactory(testEmbeddableFactory);
expect(getReactEmbeddableFactory('test')).toBe(testEmbeddableFactory);
const returnedFactory = getTestEmbeddableFactory();
registerReactEmbeddableFactory('test', getTestEmbeddableFactory);
expect(getReactEmbeddableFactory('test')).toEqual(returnedFactory);
});
it('can check if a factory is registered', () => {

View file

@ -9,32 +9,40 @@
import { i18n } from '@kbn/i18n';
import { DefaultEmbeddableApi, ReactEmbeddableFactory } from './types';
const registry: { [key: string]: ReactEmbeddableFactory<any, any> } = {};
const registry: { [key: string]: () => Promise<ReactEmbeddableFactory<any, any>> } = {};
/**
* Registers a new React embeddable factory. This should be called at plugin start time.
*
* @param type The key to register the factory under. This should be the same as the `type` key in the factory definition.
* @param getFactory an async function that gets the factory definition for this key. This should always async import the
* actual factory definition file to avoid polluting page load.
*/
export const registerReactEmbeddableFactory = <
StateType extends object = object,
APIType extends DefaultEmbeddableApi<StateType> = DefaultEmbeddableApi<StateType>
>(
factory: ReactEmbeddableFactory<StateType, APIType>
type: string,
getFactory: () => Promise<ReactEmbeddableFactory<StateType, APIType>>
) => {
if (registry[factory.type] !== undefined)
if (registry[type] !== undefined)
throw new Error(
i18n.translate('embeddableApi.reactEmbeddable.factoryAlreadyExistsError', {
defaultMessage: 'An embeddable factory for for type: {key} is already registered.',
values: { key: factory.type },
values: { key: type },
})
);
registry[factory.type] = factory;
registry[type] = getFactory;
};
export const reactEmbeddableRegistryHasKey = (key: string) => registry[key] !== undefined;
export const getReactEmbeddableFactory = <
export const getReactEmbeddableFactory = async <
StateType extends object = object,
ApiType extends DefaultEmbeddableApi<StateType> = DefaultEmbeddableApi<StateType>
>(
key: string
): ReactEmbeddableFactory<StateType, ApiType> => {
): Promise<ReactEmbeddableFactory<StateType, ApiType>> => {
if (registry[key] === undefined)
throw new Error(
i18n.translate('embeddableApi.reactEmbeddable.factoryNotFoundError', {
@ -42,5 +50,5 @@ export const getReactEmbeddableFactory = <
values: { key },
})
);
return registry[key];
return registry[key]();
};

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
import { setStubKibanaServices as setupPresentationPanelServices } from '@kbn/presentation-panel-plugin/public/mocks';
import { render, waitFor, screen } from '@testing-library/react';
import React from 'react';
@ -35,7 +36,7 @@ describe('react embeddable renderer', () => {
);
return {
Component: () => (
<div>
<div data-test-subj="superTestEmbeddable">
SUPER TEST COMPONENT, name: {state.name} bork: {state.bork}
</div>
),
@ -44,29 +45,38 @@ describe('react embeddable renderer', () => {
},
};
const getTestEmbeddableFactory = async () => {
return testEmbeddableFactory;
};
beforeAll(() => {
registerReactEmbeddableFactory(testEmbeddableFactory);
registerReactEmbeddableFactory('test', getTestEmbeddableFactory);
setupPresentationPanelServices();
});
it('deserializes given state', () => {
it('deserializes given state', async () => {
render(<ReactEmbeddableRenderer type={'test'} state={{ rawState: { bork: 'blorp?' } }} />);
expect(testEmbeddableFactory.deserializeState).toHaveBeenCalledWith({
rawState: { bork: 'blorp?' },
await waitFor(() => {
expect(testEmbeddableFactory.deserializeState).toHaveBeenCalledWith({
rawState: { bork: 'blorp?' },
});
});
});
it('builds the embeddable', () => {
it('builds the embeddable', async () => {
const buildEmbeddableSpy = jest.spyOn(testEmbeddableFactory, 'buildEmbeddable');
render(<ReactEmbeddableRenderer type={'test'} state={{ rawState: { bork: 'blorp?' } }} />);
expect(buildEmbeddableSpy).toHaveBeenCalledWith(
{ bork: 'blorp?' },
expect.any(Function),
expect.any(String),
undefined
);
await waitFor(() => {
expect(buildEmbeddableSpy).toHaveBeenCalledWith(
{ bork: 'blorp?' },
expect.any(Function),
expect.any(String),
undefined
);
});
});
it('builds the embeddable, providing an id', () => {
it('builds the embeddable, providing an id', async () => {
const buildEmbeddableSpy = jest.spyOn(testEmbeddableFactory, 'buildEmbeddable');
render(
<ReactEmbeddableRenderer
@ -75,15 +85,17 @@ describe('react embeddable renderer', () => {
state={{ rawState: { bork: 'blorp?' } }}
/>
);
expect(buildEmbeddableSpy).toHaveBeenCalledWith(
{ bork: 'blorp?' },
expect.any(Function),
'12345',
undefined
);
await waitFor(() => {
expect(buildEmbeddableSpy).toHaveBeenCalledWith(
{ bork: 'blorp?' },
expect.any(Function),
'12345',
undefined
);
});
});
it('builds the embeddable, providing a parent', () => {
it('builds the embeddable, providing a parent', async () => {
const buildEmbeddableSpy = jest.spyOn(testEmbeddableFactory, 'buildEmbeddable');
const parentApi = getMockPresentationContainer();
render(
@ -93,18 +105,27 @@ describe('react embeddable renderer', () => {
parentApi={parentApi}
/>
);
expect(buildEmbeddableSpy).toHaveBeenCalledWith(
{ bork: 'blorp?' },
expect.any(Function),
expect.any(String),
parentApi
);
await waitFor(() => {
expect(buildEmbeddableSpy).toHaveBeenCalledWith(
{ bork: 'blorp?' },
expect.any(Function),
expect.any(String),
parentApi
);
});
});
it('renders the given component once it resolves', () => {
render(<ReactEmbeddableRenderer type={'test'} state={{ rawState: { name: 'Kuni Garu' } }} />);
waitFor(() => {
expect(screen.findByText('SUPER TEST COMPONENT, name: Kuni Garu')).toBeInTheDocument();
it('renders the given component once it resolves', async () => {
render(
<ReactEmbeddableRenderer
type={'test'}
state={{ rawState: { name: 'Kuni Garu', bork: 'Dara' } }}
/>
);
await waitFor(() => {
expect(screen.queryByTestId<HTMLElement>('superTestEmbeddable')).toHaveTextContent(
'SUPER TEST COMPONENT, name: Kuni Garu bork: Dara'
);
});
});

View file

@ -17,11 +17,7 @@ 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,
ReactEmbeddableApiRegistration,
ReactEmbeddableFactory,
} from './types';
import { DefaultEmbeddableApi, ReactEmbeddableApiRegistration } from './types';
/**
* Renders a component from the React Embeddable registry into a Presentation Panel.
@ -50,10 +46,7 @@ export const ReactEmbeddableRenderer = <
() =>
(async () => {
const uuid = maybeId ?? generateId();
const factory = getReactEmbeddableFactory(type) as ReactEmbeddableFactory<
StateType,
ApiType
>;
const factory = await getReactEmbeddableFactory<StateType, ApiType>(type);
const registerApi = (
apiRegistration: ReactEmbeddableApiRegistration<StateType, ApiType>,
comparators: StateComparators<StateType>