mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
db941ae382
commit
b4e0a04af9
12 changed files with 272 additions and 261 deletions
|
@ -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 () => {},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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';
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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', () => {
|
||||
|
|
|
@ -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]();
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue