Switch to embeddable factory interface with optional override (#61165)

* wip

* typescript map embeddable

* More updates

* Address code review comments and update some usages in SIEM and uptime to the new types

* More clean up - carry over some of the SIEM types to maps for render tool tip

* fixes

* fixes

* Address more review comments

* fixes

* fixes

* fix jest test

* Fix visualize embeddable

* fixes after master merge

* Fixes

* Prefix variable with name "custom" to make it more obvious

* Remove layerList from input state

* fixes

* Update src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx

Co-Authored-By: Vadim Dalecky <streamich@users.noreply.github.com>

* review updates

* fixes

* update maps readme

Co-authored-by: Vadim Dalecky <streamich@users.noreply.github.com>
This commit is contained in:
Stacey Gammon 2020-04-02 14:27:51 -04:00 committed by GitHub
parent 09f1bae2ee
commit bb747abdaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 867 additions and 655 deletions

View file

@ -21,11 +21,11 @@ import { i18n } from '@kbn/i18n';
import {
IContainer,
EmbeddableInput,
EmbeddableFactory,
EmbeddableFactoryDefinition,
} from '../../../../src/plugins/embeddable/public';
import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable';
export class HelloWorldEmbeddableFactory extends EmbeddableFactory {
export class HelloWorldEmbeddableFactory implements EmbeddableFactoryDefinition {
public readonly type = HELLO_WORLD_EMBEDDABLE;
/**

View file

@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
import {
EmbeddableFactory,
EmbeddableFactoryDefinition,
ContainerInput,
EmbeddableStart,
} from '../../../../src/plugins/embeddable/public';
@ -29,22 +29,20 @@ interface StartServices {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
}
export class ListContainerFactory extends EmbeddableFactory {
export class ListContainerFactory implements EmbeddableFactoryDefinition {
public readonly type = LIST_CONTAINER;
public readonly isContainerType = true;
constructor(private getStartServices: () => Promise<StartServices>) {
super();
}
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
}
public async create(initialInput: ContainerInput) {
public create = async (initialInput: ContainerInput) => {
const { getEmbeddableFactory } = await this.getStartServices();
return new ListContainer(initialInput, getEmbeddableFactory);
}
};
public getDisplayName() {
return i18n.translate('embeddableExamples.searchableListContainer.displayName', {

View file

@ -18,7 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { IContainer, EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { IContainer, EmbeddableFactoryDefinition } from '../../../../src/plugins/embeddable/public';
import {
MultiTaskTodoEmbeddable,
MULTI_TASK_TODO_EMBEDDABLE,
@ -26,10 +26,8 @@ import {
MultiTaskTodoOutput,
} from './multi_task_todo_embeddable';
export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory<
MultiTaskTodoInput,
MultiTaskTodoOutput
> {
export class MultiTaskTodoEmbeddableFactory
implements EmbeddableFactoryDefinition<MultiTaskTodoInput, MultiTaskTodoOutput> {
public readonly type = MULTI_TASK_TODO_EMBEDDABLE;
public async isEditable() {

View file

@ -18,7 +18,10 @@
*/
import { i18n } from '@kbn/i18n';
import { EmbeddableFactory, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import {
EmbeddableFactoryDefinition,
EmbeddableStart,
} from '../../../../src/plugins/embeddable/public';
import {
SEARCHABLE_LIST_CONTAINER,
SearchableListContainer,
@ -29,22 +32,20 @@ interface StartServices {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
}
export class SearchableListContainerFactory extends EmbeddableFactory {
export class SearchableListContainerFactory implements EmbeddableFactoryDefinition {
public readonly type = SEARCHABLE_LIST_CONTAINER;
public readonly isContainerType = true;
constructor(private getStartServices: () => Promise<StartServices>) {
super();
}
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
}
public async create(initialInput: SearchableContainerInput) {
public create = async (initialInput: SearchableContainerInput) => {
const { getEmbeddableFactory } = await this.getStartServices();
return new SearchableListContainer(initialInput, getEmbeddableFactory);
}
};
public getDisplayName() {
return i18n.translate('embeddableExamples.searchableListContainer.displayName', {

View file

@ -23,7 +23,7 @@ import { OverlayStart } from 'kibana/public';
import { EuiFieldText } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
import { IContainer, EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { IContainer, EmbeddableFactoryDefinition } from '../../../../src/plugins/embeddable/public';
import { TodoEmbeddable, TODO_EMBEDDABLE, TodoInput, TodoOutput } from './todo_embeddable';
function TaskInput({ onSave }: { onSave: (task: string) => void }) {
@ -47,16 +47,11 @@ interface StartServices {
openModal: OverlayStart['openModal'];
}
export class TodoEmbeddableFactory extends EmbeddableFactory<
TodoInput,
TodoOutput,
TodoEmbeddable
> {
export class TodoEmbeddableFactory
implements EmbeddableFactoryDefinition<TodoInput, TodoOutput, TodoEmbeddable> {
public readonly type = TODO_EMBEDDABLE;
constructor(private getStartServices: () => Promise<StartServices>) {
super();
}
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
return true;
@ -72,7 +67,7 @@ export class TodoEmbeddableFactory extends EmbeddableFactory<
* used to collect specific embeddable input that the container will not provide, like
* in this case, the task string.
*/
public async getExplicitInput() {
public getExplicitInput = async () => {
const { openModal } = await this.getStartServices();
return new Promise<{ task: string }>(resolve => {
const onSave = (task: string) => resolve({ task });
@ -87,7 +82,7 @@ export class TodoEmbeddableFactory extends EmbeddableFactory<
)
);
});
}
};
public getDisplayName() {
return i18n.translate('embeddableExamples.todo.displayName', {

View file

@ -37,9 +37,14 @@ import {
import {
TodoEmbeddable,
TODO_EMBEDDABLE,
TodoEmbeddableFactory,
TodoInput,
} from '../../../examples/embeddable_examples/public/todo';
import { EmbeddableStart, EmbeddableRoot } from '../../../src/plugins/embeddable/public';
import {
EmbeddableStart,
EmbeddableRoot,
EmbeddableOutput,
ErrorEmbeddable,
} from '../../../src/plugins/embeddable/public';
interface Props {
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
@ -53,7 +58,7 @@ interface State {
}
export class TodoEmbeddableExample extends React.Component<Props, State> {
private embeddable?: TodoEmbeddable;
private embeddable?: TodoEmbeddable | ErrorEmbeddable;
constructor(props: Props) {
super(props);
@ -62,7 +67,9 @@ export class TodoEmbeddableExample extends React.Component<Props, State> {
}
public componentDidMount() {
const factory = this.props.getEmbeddableFactory(TODO_EMBEDDABLE) as TodoEmbeddableFactory;
const factory = this.props.getEmbeddableFactory<TodoInput, EmbeddableOutput, TodoEmbeddable>(
TODO_EMBEDDABLE
);
if (factory === undefined) {
throw new Error('Embeddable factory is undefined!');

View file

@ -48,7 +48,6 @@ import {
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
DashboardContainerFactory,
DashboardContainerInput,
DashboardPanelState,
} from '../../../../../../plugins/dashboard/public';
@ -58,6 +57,7 @@ import {
isErrorEmbeddable,
openAddPanelFlyout,
ViewMode,
ContainerOutput,
} from '../../../../../../plugins/embeddable/public';
import { NavAction, SavedDashboardPanel } from './types';
@ -307,83 +307,92 @@ export class DashboardAppController {
let outputSubscription: Subscription | undefined;
const dashboardDom = document.getElementById('dashboardViewport');
const dashboardFactory = embeddable.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
) as DashboardContainerFactory;
dashboardFactory
.create(getDashboardInput())
.then((container: DashboardContainer | ErrorEmbeddable) => {
if (!isErrorEmbeddable(container)) {
dashboardContainer = container;
const dashboardFactory = embeddable.getEmbeddableFactory<
DashboardContainerInput,
ContainerOutput,
DashboardContainer
>(DASHBOARD_CONTAINER_TYPE);
dashboardContainer.renderEmpty = () => {
const shouldShowEditHelp = getShouldShowEditHelp();
const shouldShowViewHelp = getShouldShowViewHelp();
const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState();
const isEmptyState = shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode;
return isEmptyState ? (
<DashboardEmptyScreen
{...getEmptyScreenProps(shouldShowEditHelp, isEmptyInReadOnlyMode)}
/>
) : null;
};
if (dashboardFactory) {
dashboardFactory
.create(getDashboardInput())
.then((container: DashboardContainer | ErrorEmbeddable | undefined) => {
if (container && !isErrorEmbeddable(container)) {
dashboardContainer = container;
updateIndexPatterns(dashboardContainer);
dashboardContainer.renderEmpty = () => {
const shouldShowEditHelp = getShouldShowEditHelp();
const shouldShowViewHelp = getShouldShowViewHelp();
const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState();
const isEmptyState =
shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode;
return isEmptyState ? (
<DashboardEmptyScreen
{...getEmptyScreenProps(shouldShowEditHelp, isEmptyInReadOnlyMode)}
/>
) : null;
};
outputSubscription = dashboardContainer.getOutput$().subscribe(() => {
updateIndexPatterns(dashboardContainer);
});
inputSubscription = dashboardContainer.getInput$().subscribe(() => {
let dirty = false;
// This has to be first because handleDashboardContainerChanges causes
// appState.save which will cause refreshDashboardContainer to be called.
if (
!esFilters.compareFilters(
container.getInput().filters,
queryFilter.getFilters(),
esFilters.COMPARE_ALL_OPTIONS
)
) {
// Add filters modifies the object passed to it, hence the clone deep.
queryFilter.addFilters(_.cloneDeep(container.getInput().filters));
dashboardStateManager.applyFilters($scope.model.query, container.getInput().filters);
dirty = true;
}
dashboardStateManager.handleDashboardContainerChanges(container);
$scope.$evalAsync(() => {
if (dirty) {
updateState();
}
outputSubscription = dashboardContainer.getOutput$().subscribe(() => {
updateIndexPatterns(dashboardContainer);
});
});
dashboardStateManager.registerChangeListener(() => {
// we aren't checking dirty state because there are changes the container needs to know about
// that won't make the dashboard "dirty" - like a view mode change.
refreshDashboardContainer();
});
inputSubscription = dashboardContainer.getInput$().subscribe(() => {
let dirty = false;
// This code needs to be replaced with a better mechanism for adding new embeddables of
// any type from the add panel. Likely this will happen via creating a visualization "inline",
// without navigating away from the UX.
if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) {
const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE];
const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID];
container.addSavedObjectEmbeddable(type, id);
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE);
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID);
// This has to be first because handleDashboardContainerChanges causes
// appState.save which will cause refreshDashboardContainer to be called.
if (
!esFilters.compareFilters(
container.getInput().filters,
queryFilter.getFilters(),
esFilters.COMPARE_ALL_OPTIONS
)
) {
// Add filters modifies the object passed to it, hence the clone deep.
queryFilter.addFilters(_.cloneDeep(container.getInput().filters));
dashboardStateManager.applyFilters(
$scope.model.query,
container.getInput().filters
);
dirty = true;
}
dashboardStateManager.handleDashboardContainerChanges(container);
$scope.$evalAsync(() => {
if (dirty) {
updateState();
}
});
});
dashboardStateManager.registerChangeListener(() => {
// we aren't checking dirty state because there are changes the container needs to know about
// that won't make the dashboard "dirty" - like a view mode change.
refreshDashboardContainer();
});
// This code needs to be replaced with a better mechanism for adding new embeddables of
// any type from the add panel. Likely this will happen via creating a visualization "inline",
// without navigating away from the UX.
if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) {
const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE];
const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID];
container.addSavedObjectEmbeddable(type, id);
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE);
removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID);
}
}
}
if (dashboardDom) {
container.render(dashboardDom);
}
});
if (dashboardDom && container) {
container.render(dashboardDom);
}
});
}
// Part of the exposed plugin API - do not remove without careful consideration.
this.appStatus = {

View file

@ -22,9 +22,9 @@ import { i18n } from '@kbn/i18n';
import { UiActionsStart } from 'src/plugins/ui_actions/public';
import { getServices } from '../../kibana_services';
import {
EmbeddableFactory,
ErrorEmbeddable,
EmbeddableFactoryDefinition,
Container,
ErrorEmbeddable,
} from '../../../../../../../plugins/embeddable/public';
import { TimeRange } from '../../../../../../../plugins/data/public';
@ -37,28 +37,23 @@ interface StartServices {
isEditable: () => boolean;
}
export class SearchEmbeddableFactory extends EmbeddableFactory<
SearchInput,
SearchOutput,
SearchEmbeddable
> {
export class SearchEmbeddableFactory
implements EmbeddableFactoryDefinition<SearchInput, SearchOutput, SearchEmbeddable> {
public readonly type = SEARCH_EMBEDDABLE_TYPE;
private $injector: auto.IInjectorService | null;
private getInjector: () => Promise<auto.IInjectorService> | null;
public readonly savedObjectMetaData = {
name: i18n.translate('kbn.discover.savedSearch.savedObjectName', {
defaultMessage: 'Saved search',
}),
type: 'search',
getIconForSavedObject: () => 'search',
};
constructor(
private getStartServices: () => Promise<StartServices>,
getInjector: () => Promise<auto.IInjectorService>
) {
super({
savedObjectMetaData: {
name: i18n.translate('kbn.discover.savedSearch.savedObjectName', {
defaultMessage: 'Saved search',
}),
type: 'search',
getIconForSavedObject: () => 'search',
},
});
this.$injector = null;
this.getInjector = getInjector;
}
@ -67,9 +62,9 @@ export class SearchEmbeddableFactory extends EmbeddableFactory<
return false;
}
public async isEditable() {
public isEditable = async () => {
return (await this.getStartServices()).isEditable();
}
};
public getDisplayName() {
return i18n.translate('kbn.embeddable.search.displayName', {
@ -77,11 +72,11 @@ export class SearchEmbeddableFactory extends EmbeddableFactory<
});
}
public async createFromSavedObject(
public createFromSavedObject = async (
savedObjectId: string,
input: Partial<SearchInput> & { id: string; timeRange: TimeRange },
parent?: Container
): Promise<SearchEmbeddable | ErrorEmbeddable> {
): Promise<SearchEmbeddable | ErrorEmbeddable> => {
if (!this.$injector) {
this.$injector = await this.getInjector();
}
@ -115,7 +110,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory<
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);
}
}
};
public async create(input: SearchInput) {
return new ErrorEmbeddable('Saved searches can only be created from a saved object', input);

View file

@ -61,6 +61,7 @@ export interface VisualizeKibanaServices {
I18nContext: I18nStart['Context'];
setActiveUrl: (newUrl: string) => void;
DefaultVisualizationEditor: typeof DefaultEditorController;
createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject'];
}
let services: VisualizeKibanaServices | null = null;

View file

@ -43,7 +43,7 @@ import {
import { createSavedSearchesLoader } from '../../../../../../plugins/discover/public';
const getResolvedResults = deps => {
const { core, data, visualizations } = deps;
const { core, data, visualizations, createVisEmbeddableFromObject } = deps;
const results = {};
@ -60,7 +60,7 @@ const getResolvedResults = deps => {
})
.then(vis => {
results.vis = vis;
return deps.embeddable.getEmbeddableFactory('visualization').createFromObject(results.vis, {
return createVisEmbeddableFromObject(vis, {
timeRange: data.query.timefilter.timefilter.getTime(),
filters: data.query.filterManager.getFilters(),
});

View file

@ -156,6 +156,7 @@ export class VisualizePlugin implements Plugin {
I18nContext: coreStart.i18n.Context,
setActiveUrl,
DefaultVisualizationEditor: DefaultEditorController,
createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject,
};
setServices(deps);

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_plugin';
import { isErrorEmbeddable } from '../embeddable_plugin';
import { ExpandPanelAction } from './expand_panel_action';
import { DashboardContainer } from '../embeddable';
import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
@ -29,11 +29,16 @@ import {
ContactCardEmbeddableOutput,
} from '../embeddable_plugin_test_samples';
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(
// eslint-disable-next-line
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const start = doStart();
let container: DashboardContainer;
let embeddable: ContactCardEmbeddable;
@ -43,9 +48,7 @@ beforeEach(async () => {
ExitFullScreenButton: () => null,
SavedObjectFinder: () => null,
application: {} as any,
embeddable: {
getEmbeddableFactory: (id: string) => embeddableFactories.get(id)!,
} as any,
embeddable: start,
inspector: {} as any,
notifications: {} as any,
overlays: {} as any,

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_plugin';
import { isErrorEmbeddable } from '../embeddable_plugin';
import { ReplacePanelAction } from './replace_panel_action';
import { DashboardContainer } from '../embeddable';
import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
@ -30,12 +30,15 @@ import {
import { coreMock } from '../../../../core/public/mocks';
import { CoreStart } from 'kibana/public';
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(
// eslint-disable-next-line
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const getEmbeddableFactories = () => embeddableFactories.values();
const start = doStart();
let container: DashboardContainer;
let embeddable: ContactCardEmbeddable;
@ -46,9 +49,7 @@ beforeEach(async () => {
ExitFullScreenButton: () => null,
SavedObjectFinder: () => null,
application: {} as any,
embeddable: {
getEmbeddableFactory: (id: string) => embeddableFactories.get(id)!,
} as any,
embeddable: start,
inspector: {} as any,
notifications: {} as any,
overlays: coreStart.overlays,
@ -87,7 +88,7 @@ test('Executes the replace panel action', async () => {
coreStart,
SavedObjectFinder,
notifications,
getEmbeddableFactories
start.getEmbeddableFactories
);
action.execute({ embeddable });
});
@ -99,7 +100,7 @@ test('Is not compatible when embeddable is not in a dashboard container', async
coreStart,
SavedObjectFinder,
notifications,
getEmbeddableFactories
start.getEmbeddableFactories
);
expect(
await action.isCompatible({
@ -118,7 +119,7 @@ test('Execute throws an error when called with an embeddable not in a parent', a
coreStart,
SavedObjectFinder,
notifications,
getEmbeddableFactories
start.getEmbeddableFactories
);
async function check() {
await action.execute({ embeddable: container });
@ -133,7 +134,7 @@ test('Returns title', async () => {
coreStart,
SavedObjectFinder,
notifications,
getEmbeddableFactories
start.getEmbeddableFactories
);
expect(action.getDisplayName({ embeddable })).toBeDefined();
});
@ -145,7 +146,7 @@ test('Returns an icon', async () => {
coreStart,
SavedObjectFinder,
notifications,
getEmbeddableFactories
start.getEmbeddableFactories
);
expect(action.getIconType({ embeddable })).toBeDefined();
});

View file

@ -20,7 +20,7 @@
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { nextTick } from 'test_utils/enzyme_helpers';
import { isErrorEmbeddable, ViewMode, EmbeddableFactory } from '../embeddable_plugin';
import { isErrorEmbeddable, ViewMode } from '../embeddable_plugin';
import { DashboardContainer, DashboardContainerOptions } from './dashboard_container';
import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
import {
@ -30,14 +30,12 @@ import {
ContactCardEmbeddable,
ContactCardEmbeddableOutput,
} from '../embeddable_plugin_test_samples';
// eslint-disable-next-line
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
const options: DashboardContainerOptions = {
application: {} as any,
embeddable: {
getTriggerCompatibleActions: (() => []) as any,
getEmbeddableFactories: (() => []) as any,
getEmbeddableFactory: undefined as any,
} as any,
embeddable: {} as any,
notifications: {} as any,
overlays: {} as any,
inspector: {} as any,
@ -47,12 +45,12 @@ const options: DashboardContainerOptions = {
};
beforeEach(() => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
options.embeddable.getEmbeddableFactory = (id: string) => embeddableFactories.get(id) as any;
options.embeddable = doStart();
});
test('DashboardContainer initializes embeddables', async done => {

View file

@ -23,7 +23,7 @@ import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public';
import { CoreStart } from '../../../../core/public';
import {
ContainerOutput,
EmbeddableFactory,
EmbeddableFactoryDefinition,
ErrorEmbeddable,
Container,
} from '../embeddable_plugin';
@ -43,27 +43,24 @@ interface StartServices {
uiActions: UiActionsStart;
}
export class DashboardContainerFactory extends EmbeddableFactory<
DashboardContainerInput,
ContainerOutput
> {
export class DashboardContainerFactory
implements
EmbeddableFactoryDefinition<DashboardContainerInput, ContainerOutput, DashboardContainer> {
public readonly isContainerType = true;
public readonly type = DASHBOARD_CONTAINER_TYPE;
constructor(private readonly getStartServices: () => Promise<StartServices>) {
super();
}
constructor(private readonly getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
public isEditable = async () => {
const { capabilities } = await this.getStartServices();
return !!capabilities.createNew && !!capabilities.showWriteControls;
}
};
public getDisplayName() {
public readonly getDisplayName = () => {
return i18n.translate('dashboard.factory.displayName', {
defaultMessage: 'dashboard',
});
}
};
public getDefaultInput(): Partial<DashboardContainerInput> {
return {
@ -73,11 +70,11 @@ export class DashboardContainerFactory extends EmbeddableFactory<
};
}
public async create(
public create = async (
initialInput: DashboardContainerInput,
parent?: Container
): Promise<DashboardContainer | ErrorEmbeddable> {
): Promise<DashboardContainer | ErrorEmbeddable> => {
const services = await this.getStartServices();
return new DashboardContainer(initialInput, services, parent);
}
};
}

View file

@ -23,7 +23,6 @@ import sizeMe from 'react-sizeme';
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { skip } from 'rxjs/operators';
import { EmbeddableFactory } from '../../embeddable_plugin';
import { DashboardGrid, DashboardGridProps } from './dashboard_grid';
import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container';
import { getSampleDashboardInput } from '../../test_helpers';
@ -32,16 +31,20 @@ import {
ContactCardEmbeddableFactory,
} from '../../embeddable_plugin_test_samples';
import { KibanaContextProvider } from '../../../../kibana_react/public';
// eslint-disable-next-line
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
let dashboardContainer: DashboardContainer | undefined;
function prepare(props?: Partial<DashboardGridProps>) {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({} as any, (() => {}) as any, {} as any)
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
const start = doStart();
const getEmbeddableFactory = start.getEmbeddableFactory;
const initialInput = getSampleDashboardInput({
panels: {
'1': {
@ -60,7 +63,7 @@ function prepare(props?: Partial<DashboardGridProps>) {
application: {} as any,
embeddable: {
getTriggerCompatibleActions: (() => []) as any,
getEmbeddableFactories: (() => []) as any,
getEmbeddableFactories: start.getEmbeddableFactories,
getEmbeddableFactory,
} as any,
notifications: {} as any,

View file

@ -24,7 +24,6 @@ import { skip } from 'rxjs/operators';
import { mount } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { nextTick } from 'test_utils/enzyme_helpers';
import { EmbeddableFactory } from '../../embeddable_plugin';
import { DashboardViewport, DashboardViewportProps } from './dashboard_viewport';
import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container';
import { getSampleDashboardInput } from '../../test_helpers';
@ -33,6 +32,8 @@ import {
ContactCardEmbeddableFactory,
} from '../../embeddable_plugin_test_samples';
import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public';
// eslint-disable-next-line
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
let dashboardContainer: DashboardContainer | undefined;
@ -41,18 +42,19 @@ const ExitFullScreenButton = () => <div data-test-subj="exitFullScreenModeText">
function getProps(
props?: Partial<DashboardViewportProps>
): { props: DashboardViewportProps; options: DashboardContainerOptions } {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({}, (() => null) as any, {} as any)
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const start = doStart();
const options: DashboardContainerOptions = {
application: {} as any,
embeddable: {
getTriggerCompatibleActions: (() => []) as any,
getEmbeddableFactories: (() => []) as any,
getEmbeddableFactory: (id: string) => embeddableFactories.get(id),
getEmbeddableFactories: start.getEmbeddableFactories,
getEmbeddableFactory: start.getEmbeddableFactory,
} as any,
notifications: {} as any,
overlays: {} as any,

View file

@ -52,7 +52,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction);
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const start = doStart();

View file

@ -38,6 +38,7 @@ export {
EmbeddableChildPanel,
EmbeddableChildPanelProps,
EmbeddableContext,
EmbeddableFactoryDefinition,
EmbeddableFactory,
EmbeddableFactoryNotFoundError,
EmbeddableFactoryRenderer,

View file

@ -18,14 +18,14 @@
*/
import { EditPanelAction } from './edit_panel_action';
import { EmbeddableFactory, Embeddable, EmbeddableInput } from '../embeddables';
import { Embeddable, EmbeddableInput } from '../embeddables';
import { ViewMode } from '../types';
import { ContactCardEmbeddable } from '../test_samples';
import { EmbeddableStart } from '../../plugin';
import { embeddablePluginMock } from '../../mocks';
const embeddableFactories = new Map<string, EmbeddableFactory>();
const getFactory = ((id: string) =>
embeddableFactories.get(id)) as EmbeddableStart['getEmbeddableFactory'];
const { doStart } = embeddablePluginMock.createInstance();
const start = doStart();
const getFactory = start.getEmbeddableFactory;
class EditableEmbeddable extends Embeddable {
public readonly type = 'EDITABLE_EMBEDDABLE';
@ -83,9 +83,7 @@ test('is not compatible when edit url is not available', async () => {
});
test('is not visible when edit url is available but in view mode', async () => {
embeddableFactories.clear();
const action = new EditPanelAction((type =>
embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']);
const action = new EditPanelAction(getFactory);
expect(
await action.isCompatible({
embeddable: new EditableEmbeddable(
@ -100,9 +98,7 @@ test('is not visible when edit url is available but in view mode', async () => {
});
test('is not compatible when edit url is available, in edit mode, but not editable', async () => {
embeddableFactories.clear();
const action = new EditPanelAction((type =>
embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']);
const action = new EditPanelAction(getFactory);
expect(
await action.isCompatible({
embeddable: new EditableEmbeddable(

View file

@ -20,7 +20,6 @@
import React from 'react';
import { nextTick } from 'test_utils/enzyme_helpers';
import { EmbeddableChildPanel } from './embeddable_child_panel';
import { EmbeddableFactory } from '../embeddables';
import { CONTACT_CARD_EMBEDDABLE } from '../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { SlowContactCardEmbeddableFactory } from '../test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory';
import { HelloWorldContainer } from '../test_samples/embeddables/hello_world_container';
@ -32,16 +31,17 @@ import {
// eslint-disable-next-line
import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
import { mount } from 'enzyme';
import { embeddablePluginMock } from '../../mocks';
test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => {
const inspector = inspectorPluginMock.createStartContract();
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new SlowContactCardEmbeddableFactory({ execAction: (() => null) as any })
);
const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
const start = doStart();
const getEmbeddableFactory = start.getEmbeddableFactory;
const container = new HelloWorldContainer({ id: 'hello', panels: {} }, {
getEmbeddableFactory,
@ -63,8 +63,8 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async
container={container}
embeddableId={newEmbeddable.id}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
getAllEmbeddableFactories={start.getEmbeddableFactories}
getEmbeddableFactory={getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SavedObjectAttributes } from 'kibana/public';
import { EmbeddableFactoryDefinition } from './embeddable_factory_definition';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { EmbeddableFactory } from './embeddable_factory';
import { IContainer } from '..';
export const defaultEmbeddableFactoryProvider = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>,
T extends SavedObjectAttributes = SavedObjectAttributes
>(
def: EmbeddableFactoryDefinition<I, O, E, T>
): EmbeddableFactory<I, O, E, T> => {
const factory: EmbeddableFactory<I, O, E, T> = {
isContainerType: def.isContainerType ?? false,
canCreateNew: def.canCreateNew ? def.canCreateNew.bind(def) : () => true,
getDefaultInput: def.getDefaultInput ? def.getDefaultInput.bind(def) : () => ({}),
getExplicitInput: def.getExplicitInput
? def.getExplicitInput.bind(def)
: () => Promise.resolve({}),
createFromSavedObject:
def.createFromSavedObject ??
((savedObjectId: string, input: Partial<I>, parent?: IContainer) => {
throw new Error(`Creation from saved object not supported by type ${def.type}`);
}),
create: def.create.bind(def),
type: def.type,
isEditable: def.isEditable.bind(def),
getDisplayName: def.getDisplayName.bind(def),
savedObjectMetaData: def.savedObjectMetaData,
};
return factory;
};

View file

@ -158,6 +158,8 @@ export abstract class Embeddable<
*/
public destroy(): void {
this.destoyed = true;
this.input$.complete();
this.output$.complete();
if (this.parentSubscription) {
this.parentSubscription.unsubscribe();
}

View file

@ -22,32 +22,21 @@ import { SavedObjectMetaData } from '../../../../saved_objects/public';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { ErrorEmbeddable } from './error_embeddable';
import { IContainer } from '../containers/i_container';
import { PropertySpec } from '../types';
export interface EmbeddableInstanceConfiguration {
id: string;
savedObjectId?: string;
}
export interface PropertySpec {
displayName: string;
accessPath: string;
id: string;
description: string;
value?: string;
}
export interface OutputSpec {
[key: string]: PropertySpec;
}
export interface EmbeddableFactoryOptions<T extends SavedObjectAttributes> {
savedObjectMetaData?: SavedObjectMetaData<T>;
}
/**
* The EmbeddableFactory creates and initializes an embeddable instance
* EmbeddableFactories create and initialize an embeddable instance
*/
export abstract class EmbeddableFactory<
export interface EmbeddableFactory<
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput,
TEmbeddable extends IEmbeddable<TEmbeddableInput, TEmbeddableOutput> = IEmbeddable<
@ -58,9 +47,15 @@ export abstract class EmbeddableFactory<
> {
// A unique identified for this factory, which will be used to map an embeddable spec to
// a factory that can generate an instance of it.
public abstract readonly type: string;
readonly type: string;
public readonly savedObjectMetaData?: SavedObjectMetaData<TSavedObjectAttributes>;
/**
* Returns whether the current user should be allowed to edit this type of
* embeddable. Most of the time this should be based off the capabilities service, hence it's async.
*/
readonly isEditable: () => Promise<boolean>;
readonly savedObjectMetaData?: SavedObjectMetaData<TSavedObjectAttributes>;
/**
* True if is this factory create embeddables that are Containers. Used in the add panel to
@ -68,31 +63,19 @@ export abstract class EmbeddableFactory<
* supported right now, but once nested containers are officially supported we can probably get
* rid of this interface.
*/
public readonly isContainerType: boolean = false;
constructor({ savedObjectMetaData }: EmbeddableFactoryOptions<TSavedObjectAttributes> = {}) {
this.savedObjectMetaData = savedObjectMetaData;
}
/**
* Returns whether the current user should be allowed to edit this type of
* embeddable. Most of the time this should be based off the capabilities service, hence it's async.
*/
public abstract async isEditable(): Promise<boolean>;
readonly isContainerType: boolean;
/**
* Returns a display name for this type of embeddable. Used in "Create new... " options
* in the add panel for containers.
*/
public abstract getDisplayName(): string;
getDisplayName(): string;
/**
* If false, this type of embeddable can't be created with the "createNew" functionality. Instead,
* use createFromSavedObject, where an existing saved object must first exist.
*/
public canCreateNew() {
return true;
}
canCreateNew(): boolean;
/**
* Can be used to get any default input, to be passed in to during the creation process. Default
@ -100,18 +83,14 @@ export abstract class EmbeddableFactory<
* default input parameters.
* @param partial
*/
public getDefaultInput(partial: Partial<TEmbeddableInput>): Partial<TEmbeddableInput> {
return {};
}
getDefaultInput(partial: Partial<TEmbeddableInput>): Partial<TEmbeddableInput>;
/**
* Can be used to request explicit input from the user, to be passed in to `EmbeddableFactory:create`.
* Explicit input is stored on the parent container for this embeddable. It overrides any inherited
* input passed down from the parent container.
*/
public async getExplicitInput(): Promise<Partial<TEmbeddableInput>> {
return {};
}
getExplicitInput(): Promise<Partial<TEmbeddableInput>>;
/**
* Creates a new embeddable instance based off the saved object id.
@ -120,13 +99,11 @@ export abstract class EmbeddableFactory<
* range of the parent container.
* @param parent
*/
public createFromSavedObject(
createFromSavedObject(
savedObjectId: string,
input: Partial<TEmbeddableInput>,
parent?: IContainer
): Promise<TEmbeddable | ErrorEmbeddable> {
throw new Error(`Creation from saved object not supported by type ${this.type}`);
}
): Promise<TEmbeddable | ErrorEmbeddable>;
/**
* Resolves to undefined if a new Embeddable cannot be directly created and the user will instead be redirected
@ -134,7 +111,7 @@ export abstract class EmbeddableFactory<
*
* This will likely change in future iterations when we improve in place editing capabilities.
*/
public abstract create(
create(
initialInput: TEmbeddableInput,
parent?: IContainer
): Promise<TEmbeddable | ErrorEmbeddable | undefined>;

View file

@ -0,0 +1,44 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SavedObjectAttributes } from 'kibana/server';
import { IEmbeddable } from './i_embeddable';
import { EmbeddableFactory } from './embeddable_factory';
import { EmbeddableInput, EmbeddableOutput } from '..';
export type EmbeddableFactoryDefinition<
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>,
T extends SavedObjectAttributes = SavedObjectAttributes
> =
// Required parameters
Pick<EmbeddableFactory<I, O, E, T>, 'create' | 'type' | 'isEditable' | 'getDisplayName'> &
// Optional parameters
Partial<
Pick<
EmbeddableFactory<I, O, E, T>,
| 'createFromSavedObject'
| 'isContainerType'
| 'getExplicitInput'
| 'savedObjectMetaData'
| 'canCreateNew'
| 'getDefaultInput'
>
>;

View file

@ -21,22 +21,22 @@ import {
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactory,
} from '../../../../../../examples/embeddable_examples/public';
import { EmbeddableFactory } from './embeddable_factory';
import { EmbeddableFactoryRenderer } from './embeddable_factory_renderer';
import { mount } from 'enzyme';
import { nextTick } from 'test_utils/enzyme_helpers';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { EmbeddableStart } from '../../plugin';
import { embeddablePluginMock } from '../../mocks';
test('EmbeddableFactoryRenderer renders an embeddable', async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
const getEmbeddableFactory = doStart().getEmbeddableFactory;
const component = mount(
<EmbeddableFactoryRenderer
getEmbeddableFactory={getEmbeddableFactory as EmbeddableStart['getEmbeddableFactory']}
getEmbeddableFactory={getEmbeddableFactory}
type={HELLO_WORLD_EMBEDDABLE}
input={{ id: '123' }}
/>

View file

@ -18,11 +18,9 @@
*/
export { EmbeddableOutput, EmbeddableInput, IEmbeddable } from './i_embeddable';
export { Embeddable } from './embeddable';
export {
EmbeddableInstanceConfiguration,
EmbeddableFactory,
OutputSpec,
} from './embeddable_factory';
export * from './embeddable_factory';
export * from './embeddable_factory_definition';
export * from './default_embeddable_factory_provider';
export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
export { withEmbeddableSubscription } from './with_subscription';
export { EmbeddableFactoryRenderer } from './embeddable_factory_renderer';

View file

@ -27,7 +27,7 @@ import { I18nProvider } from '@kbn/i18n/react';
import { CONTEXT_MENU_TRIGGER } from '../triggers';
import { Action, UiActionsStart, ActionType } from 'src/plugins/ui_actions/public';
import { Trigger, ViewMode } from '../types';
import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables';
import { isErrorEmbeddable } from '../embeddables';
import { EmbeddablePanel } from './embeddable_panel';
import { createEditModeAction } from '../test_samples/actions';
import {
@ -43,26 +43,25 @@ import {
// eslint-disable-next-line
import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
import { EuiBadge } from '@elastic/eui';
import { embeddablePluginMock } from '../../mocks';
const actionRegistry = new Map<string, Action<object | undefined | string | number>>();
const triggerRegistry = new Map<string, Trigger>();
const embeddableFactories = new Map<string, EmbeddableFactory>();
const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
const { setup, doStart } = embeddablePluginMock.createInstance();
const editModeAction = createEditModeAction();
const trigger: Trigger = {
id: CONTEXT_MENU_TRIGGER,
};
const embeddableFactory = new ContactCardEmbeddableFactory(
{} as any,
(() => null) as any,
{} as any
);
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
actionRegistry.set(editModeAction.id, editModeAction);
triggerRegistry.set(trigger.id, trigger);
embeddableFactories.set(embeddableFactory.type, embeddableFactory);
setup.registerEmbeddableFactory(embeddableFactory.type, embeddableFactory);
const start = doStart();
const getEmbeddableFactory = start.getEmbeddableFactory;
test('HelloWorldContainer initializes embeddables', async done => {
const container = new HelloWorldContainer(
{
@ -157,8 +156,8 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => {
<EmbeddablePanel
embeddable={embeddable}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
getAllEmbeddableFactories={start.getEmbeddableFactories}
getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@ -195,8 +194,8 @@ const renderInEditModeAndOpenContextMenu = async (
<EmbeddablePanel
embeddable={embeddable}
getActions={getActions}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
getAllEmbeddableFactories={start.getEmbeddableFactories}
getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@ -293,8 +292,8 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
<EmbeddablePanel
embeddable={embeddable}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
getAllEmbeddableFactories={start.getEmbeddableFactories}
getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@ -355,8 +354,8 @@ test('Updates when hidePanelTitles is toggled', async () => {
<EmbeddablePanel
embeddable={embeddable}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
getAllEmbeddableFactories={start.getEmbeddableFactories}
getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@ -407,8 +406,8 @@ test('Check when hide header option is false', async () => {
<EmbeddablePanel
embeddable={embeddable}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
getAllEmbeddableFactories={start.getEmbeddableFactories}
getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}
@ -444,8 +443,8 @@ test('Check when hide header option is true', async () => {
<EmbeddablePanel
embeddable={embeddable}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
getAllEmbeddableFactories={start.getEmbeddableFactories}
getEmbeddableFactory={start.getEmbeddableFactory}
notifications={{} as any}
overlays={{} as any}
inspector={inspector}

View file

@ -19,7 +19,6 @@
import { ViewMode, EmbeddableOutput, isErrorEmbeddable } from '../../../../';
import { AddPanelAction } from './add_panel_action';
import { EmbeddableFactory } from '../../../../embeddables';
import {
FILTERABLE_EMBEDDABLE,
FilterableEmbeddable,
@ -31,11 +30,12 @@ import { FilterableContainer } from '../../../../test_samples/embeddables/filter
import { coreMock } from '../../../../../../../../core/public/mocks';
import { ContactCardEmbeddable } from '../../../../test_samples';
import { esFilters, Filter } from '../../../../../../../../plugins/data/public';
import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
import { EmbeddableStart } from '../../../../../plugin';
import { embeddablePluginMock } from '../../../../../mocks';
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = (id: string) => embeddableFactories.get(id);
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = doStart().getEmbeddableFactory;
let container: FilterableContainer;
let embeddable: FilterableEmbeddable;

View file

@ -31,7 +31,7 @@ import { ReactWrapper } from 'enzyme';
import { coreMock } from '../../../../../../../../core/public/mocks';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
import { embeddablePluginMock } from '../../../../../mocks';
function DummySavedObjectFinder(props: { children: React.ReactNode }) {
return (
@ -43,10 +43,10 @@ function DummySavedObjectFinder(props: { children: React.ReactNode }) {
}
test('createNewEmbeddable() add embeddable to container', async () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
const core = coreMock.createStart();
const { overlays } = core;
const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
{},
(() => null) as any,
overlays
);
@ -55,7 +55,9 @@ test('createNewEmbeddable() add embeddable to container', async () => {
firstName: 'foo',
lastName: 'bar',
} as any);
const getEmbeddableFactory = (id: string) => contactCardEmbeddableFactory;
setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory);
const start = doStart();
const getEmbeddableFactory = start.getEmbeddableFactory;
const input: ContainerInput<{ firstName: string; lastName: string }> = {
id: '1',
panels: {},
@ -66,8 +68,8 @@ test('createNewEmbeddable() add embeddable to container', async () => {
<AddPanelFlyout
container={container}
onClose={onClose}
getFactory={getEmbeddableFactory as EmbeddableStart['getEmbeddableFactory']}
getAllFactories={() => new Set<any>([contactCardEmbeddableFactory]).values()}
getFactory={getEmbeddableFactory}
getAllFactories={start.getEmbeddableFactories}
notifications={core.notifications}
SavedObjectFinder={() => null}
/>
@ -88,10 +90,10 @@ test('createNewEmbeddable() add embeddable to container', async () => {
});
test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()', async () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
const core = coreMock.createStart();
const { overlays } = core;
const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
{},
(() => null) as any,
overlays
);
@ -100,8 +102,10 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()'
firstName: 'foo',
lastName: 'bar',
} as any);
const getEmbeddableFactory = ((id: string) =>
contactCardEmbeddableFactory) as EmbeddableStart['getEmbeddableFactory'];
setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory);
const start = doStart();
const getEmbeddableFactory = start.getEmbeddableFactory;
const input: ContainerInput<{ firstName: string; lastName: string }> = {
id: '1',
panels: {},
@ -113,7 +117,7 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()'
container={container}
onClose={onClose}
getFactory={getEmbeddableFactory}
getAllFactories={() => new Set<any>([contactCardEmbeddableFactory]).values()}
getAllFactories={start.getEmbeddableFactories}
notifications={core.notifications}
SavedObjectFinder={props => <DummySavedObjectFinder {...props} />}
/>

View file

@ -121,15 +121,16 @@ export class AddPanelFlyout extends React.Component<Props, State> {
public render() {
const SavedObjectFinder = this.props.SavedObjectFinder;
const metaData = [...this.props.getAllFactories()]
.filter(
embeddableFactory =>
Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
)
.map(({ savedObjectMetaData }) => savedObjectMetaData as any);
const savedObjectsFinder = (
<SavedObjectFinder
onChoose={this.onAddPanel}
savedObjectMetaData={[...this.props.getAllFactories()]
.filter(
embeddableFactory =>
Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
)
.map(({ savedObjectMetaData }) => savedObjectMetaData as any)}
savedObjectMetaData={metaData}
showFilter={true}
noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', {
defaultMessage: 'No matching objects found.',

View file

@ -32,18 +32,19 @@ import {
ContactCardEmbeddableFactory,
} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container';
import { EmbeddableFactory } from '../../../../embeddables';
import { embeddablePluginMock } from '../../../../../mocks';
let container: Container;
let embeddable: ContactCardEmbeddable;
function createHelloWorldContainer(input = { id: '123', panels: {} }) {
const embeddableFactories = new Map<string, EmbeddableFactory>();
const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
embeddableFactories.set(
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({}, (() => {}) as any, {} as any)
new ContactCardEmbeddableFactory((() => {}) as any, {} as any)
);
const getEmbeddableFactory = doStart().getEmbeddableFactory;
return new HelloWorldContainer(input, { getEmbeddableFactory } as any);
}

View file

@ -28,20 +28,16 @@ import {
} from '../../../test_samples';
// eslint-disable-next-line
import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
import {
EmbeddableFactory,
EmbeddableOutput,
isErrorEmbeddable,
ErrorEmbeddable,
} from '../../../embeddables';
import { EmbeddableOutput, isErrorEmbeddable, ErrorEmbeddable } from '../../../embeddables';
import { of } from '../../../../tests/helpers';
import { esFilters } from '../../../../../../../plugins/data/public';
import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
import { embeddablePluginMock } from '../../../../mocks';
import { EmbeddableStart } from '../../../../plugin';
const setup = async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = (id: string) => embeddableFactories.get(id);
const setupTests = async () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = doStart().getEmbeddableFactory;
const container = new FilterableContainer(
{
id: 'hello',
@ -79,7 +75,7 @@ test('Is compatible when inspector adapters are available', async () => {
const inspector = inspectorPluginMock.createStartContract();
inspector.isAvailable.mockImplementation(() => true);
const { embeddable } = await setup();
const { embeddable } = await setupTests();
const inspectAction = new InspectPanelAction(inspector);
expect(await inspectAction.isCompatible({ embeddable })).toBe(true);
@ -114,7 +110,7 @@ test('Executes when inspector adapters are available', async () => {
const inspector = inspectorPluginMock.createStartContract();
inspector.isAvailable.mockImplementation(() => true);
const { embeddable } = await setup();
const { embeddable } = await setupTests();
const inspectAction = new InspectPanelAction(inspector);
expect(inspector.open).toHaveBeenCalledTimes(0);

View file

@ -19,7 +19,6 @@
import { EmbeddableOutput, isErrorEmbeddable } from '../../../';
import { RemovePanelAction } from './remove_panel_action';
import { EmbeddableFactory } from '../../../embeddables';
import { EmbeddableStart } from '../../../../plugin';
import {
FILTERABLE_EMBEDDABLE,
@ -31,11 +30,11 @@ import { FilterableContainer } from '../../../test_samples/embeddables/filterabl
import { ViewMode } from '../../../types';
import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable';
import { esFilters, Filter } from '../../../../../../../plugins/data/public';
import { embeddablePluginMock } from '../../../../mocks';
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = (id: string) => embeddableFactories.get(id);
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = doStart().getEmbeddableFactory;
let container: FilterableContainer;
let embeddable: FilterableEmbeddable;

View file

@ -23,24 +23,21 @@ import { UiActionsStart } from 'src/plugins/ui_actions/public';
import { CoreStart } from 'src/core/public';
import { toMountPoint } from '../../../../../../kibana_react/public';
import { EmbeddableFactory } from '../../../embeddables';
import { EmbeddableFactoryDefinition } from '../../../embeddables';
import { Container } from '../../../containers';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
import { ContactCardInitializer } from './contact_card_initializer';
import { EmbeddableFactoryOptions } from '../../../embeddables/embeddable_factory';
export const CONTACT_CARD_EMBEDDABLE = 'CONTACT_CARD_EMBEDDABLE';
export class ContactCardEmbeddableFactory extends EmbeddableFactory<ContactCardEmbeddableInput> {
export class ContactCardEmbeddableFactory
implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput> {
public readonly type = CONTACT_CARD_EMBEDDABLE;
constructor(
options: EmbeddableFactoryOptions<any>,
private readonly execTrigger: UiActionsStart['executeTriggerActions'],
private readonly overlays: CoreStart['overlays']
) {
super(options);
}
) {}
public async isEditable() {
return true;
@ -52,7 +49,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory<ContactCardE
});
}
public getExplicitInput(): Promise<Partial<ContactCardEmbeddableInput>> {
public getExplicitInput = (): Promise<Partial<ContactCardEmbeddableInput>> => {
return new Promise(resolve => {
const modalSession = this.overlays.openModal(
toMountPoint(
@ -72,9 +69,9 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory<ContactCardE
}
);
});
}
};
public async create(initialInput: ContactCardEmbeddableInput, parent?: Container) {
public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => {
return new ContactCardEmbeddable(
initialInput,
{
@ -82,5 +79,5 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory<ContactCardE
},
parent
);
}
};
}

View file

@ -18,7 +18,7 @@
*/
import { UiActionsStart } from 'src/plugins/ui_actions/public';
import { Container, EmbeddableFactory } from '../../..';
import { Container, EmbeddableFactoryDefinition } from '../../..';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory';
@ -27,14 +27,12 @@ interface SlowContactCardEmbeddableFactoryOptions {
loadTickCount?: number;
}
export class SlowContactCardEmbeddableFactory extends EmbeddableFactory<
ContactCardEmbeddableInput
> {
export class SlowContactCardEmbeddableFactory
implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput> {
private loadTickCount = 0;
public readonly type = CONTACT_CARD_EMBEDDABLE;
constructor(private readonly options: SlowContactCardEmbeddableFactoryOptions) {
super();
if (options.loadTickCount) {
this.loadTickCount = options.loadTickCount;
}
@ -48,10 +46,10 @@ export class SlowContactCardEmbeddableFactory extends EmbeddableFactory<
return 'slow to load contact card';
}
public async create(initialInput: ContactCardEmbeddableInput, parent?: Container) {
public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => {
for (let i = 0; i < this.loadTickCount; i++) {
await Promise.resolve();
}
return new ContactCardEmbeddable(initialInput, { execAction: this.options.execAction }, parent);
}
};
}

View file

@ -18,24 +18,21 @@
*/
import { i18n } from '@kbn/i18n';
import { Container, EmbeddableFactory } from '../..';
import { Container, EmbeddableFactoryDefinition } from '../..';
import {
FilterableContainer,
FilterableContainerInput,
FILTERABLE_CONTAINER,
} from './filterable_container';
import { EmbeddableFactoryOptions } from '../../embeddables/embeddable_factory';
import { EmbeddableStart } from '../../../plugin';
export class FilterableContainerFactory extends EmbeddableFactory<FilterableContainerInput> {
export class FilterableContainerFactory
implements EmbeddableFactoryDefinition<FilterableContainerInput> {
public readonly type = FILTERABLE_CONTAINER;
constructor(
private readonly getFactory: EmbeddableStart['getEmbeddableFactory'],
options: EmbeddableFactoryOptions<any> = {}
) {
super(options);
}
private readonly getFactory: () => Promise<EmbeddableStart['getEmbeddableFactory']>
) {}
public getDisplayName() {
return i18n.translate('embeddableApi.samples.filterableContainer.displayName', {
@ -47,7 +44,8 @@ export class FilterableContainerFactory extends EmbeddableFactory<FilterableCont
return true;
}
public async create(initialInput: FilterableContainerInput, parent?: Container) {
return new FilterableContainer(initialInput, this.getFactory, parent);
}
public create = async (initialInput: FilterableContainerInput, parent?: Container) => {
const getEmbeddableFactory = await this.getFactory();
return new FilterableContainer(initialInput, getEmbeddableFactory, parent);
};
}

View file

@ -23,10 +23,11 @@ import {
FilterableEmbeddableInput,
FILTERABLE_EMBEDDABLE,
} from './filterable_embeddable';
import { EmbeddableFactory } from '../../embeddables';
import { EmbeddableFactoryDefinition } from '../../embeddables';
import { IContainer } from '../../containers';
export class FilterableEmbeddableFactory extends EmbeddableFactory<FilterableEmbeddableInput> {
export class FilterableEmbeddableFactory
implements EmbeddableFactoryDefinition<FilterableEmbeddableInput> {
public readonly type = FILTERABLE_EMBEDDABLE;
public async isEditable() {

View file

@ -30,6 +30,7 @@ export type Start = jest.Mocked<EmbeddableStart>;
const createSetupContract = (): Setup => {
const setupContract: Setup = {
registerEmbeddableFactory: jest.fn(),
setCustomEmbeddableFactoryProvider: jest.fn(),
};
return setupContract;
};

View file

@ -18,6 +18,9 @@
*/
import { coreMock } from '../../../core/public/mocks';
import { testPlugin } from './tests/test_plugin';
import { EmbeddableFactoryProvider } from './types';
import { defaultEmbeddableFactoryProvider } from './lib';
import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/public';
test('cannot register embeddable factory with the same ID', async () => {
const coreSetup = coreMock.createSetup();
@ -33,3 +36,75 @@ test('cannot register embeddable factory with the same ID', async () => {
'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.'
);
});
test('can set custom embeddable factory provider', async () => {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart } = testPlugin(coreSetup, coreStart);
const customProvider: EmbeddableFactoryProvider = def => ({
...defaultEmbeddableFactoryProvider(def),
getDisplayName: () => 'Intercepted!',
});
setup.setCustomEmbeddableFactoryProvider(customProvider);
setup.registerEmbeddableFactory('test', {
type: 'test',
create: () => Promise.resolve(undefined),
getDisplayName: () => 'Test',
isEditable: () => Promise.resolve(true),
});
const start = doStart();
const factory = start.getEmbeddableFactory('test');
expect(factory!.getDisplayName()).toEqual('Intercepted!');
});
test('custom embeddable factory provider test for intercepting embeddable creation and destruction', async () => {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart } = testPlugin(coreSetup, coreStart);
let updateCount = 0;
const customProvider: EmbeddableFactoryProvider = def => {
return {
...defaultEmbeddableFactoryProvider(def),
create: async (input, parent) => {
const embeddable = await defaultEmbeddableFactoryProvider(def).create(input, parent);
if (embeddable) {
const subscription = embeddable.getInput$().subscribe(
() => {
updateCount++;
},
() => {},
() => {
subscription.unsubscribe();
updateCount = 0;
}
);
}
return embeddable;
},
};
};
setup.setCustomEmbeddableFactoryProvider(customProvider);
setup.registerEmbeddableFactory('test', {
type: 'test',
create: (input, parent) => Promise.resolve(new HelloWorldEmbeddable(input, parent)),
getDisplayName: () => 'Test',
isEditable: () => Promise.resolve(true),
});
const start = doStart();
const factory = start.getEmbeddableFactory('test');
const embeddable = await factory?.create({ id: '123' });
embeddable!.updateInput({ title: 'boo' });
// initial subscription, plus the second update.
expect(updateCount).toEqual(2);
embeddable!.destroy();
await new Promise(resolve => process.nextTick(resolve));
expect(updateCount).toEqual(0);
});

View file

@ -18,9 +18,16 @@
*/
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { EmbeddableFactoryRegistry } from './types';
import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types';
import { bootstrap } from './bootstrap';
import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput } from './lib';
import {
EmbeddableFactory,
EmbeddableInput,
EmbeddableOutput,
defaultEmbeddableFactoryProvider,
IEmbeddable,
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
@ -29,21 +36,29 @@ export interface EmbeddableSetupDependencies {
export interface EmbeddableSetup {
registerEmbeddableFactory: <I extends EmbeddableInput, O extends EmbeddableOutput>(
id: string,
factory: EmbeddableFactory<I, O>
factory: EmbeddableFactoryDefinition<I, O>
) => void;
setCustomEmbeddableFactoryProvider: (customProvider: EmbeddableFactoryProvider) => void;
}
export interface EmbeddableStart {
getEmbeddableFactory: <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
embeddableFactoryId: string
) => EmbeddableFactory<I, O> | undefined;
) => EmbeddableFactory<I, O, E> | undefined;
getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
}
export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, EmbeddableStart> {
private readonly embeddableFactoryDefinitions: Map<
string,
EmbeddableFactoryDefinition
> = new Map();
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
private customEmbeddableFactoryProvider?: EmbeddableFactoryProvider;
constructor(initializerContext: PluginInitializerContext) {}
@ -52,34 +67,57 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
return {
registerEmbeddableFactory: this.registerEmbeddableFactory,
setCustomEmbeddableFactoryProvider: (provider: EmbeddableFactoryProvider) => {
if (this.customEmbeddableFactoryProvider) {
throw new Error(
'Custom embeddable factory provider is already set, and can only be set once'
);
}
this.customEmbeddableFactoryProvider = provider;
},
};
}
public start(core: CoreStart) {
public start(core: CoreStart): EmbeddableStart {
this.embeddableFactoryDefinitions.forEach(def => {
this.embeddableFactories.set(
def.type,
this.customEmbeddableFactoryProvider
? this.customEmbeddableFactoryProvider(def)
: defaultEmbeddableFactoryProvider(def)
);
});
return {
getEmbeddableFactory: this.getEmbeddableFactory,
getEmbeddableFactories: () => this.embeddableFactories.values(),
getEmbeddableFactories: () => {
this.ensureFactoriesExist();
return this.embeddableFactories.values();
},
};
}
public stop() {}
private registerEmbeddableFactory = (embeddableFactoryId: string, factory: EmbeddableFactory) => {
if (this.embeddableFactories.has(embeddableFactoryId)) {
private registerEmbeddableFactory = (
embeddableFactoryId: string,
factory: EmbeddableFactoryDefinition
) => {
if (this.embeddableFactoryDefinitions.has(embeddableFactoryId)) {
throw new Error(
`Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.`
);
}
this.embeddableFactories.set(embeddableFactoryId, factory);
this.embeddableFactoryDefinitions.set(embeddableFactoryId, factory);
};
private getEmbeddableFactory = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
embeddableFactoryId: string
) => {
): EmbeddableFactory<I, O, E> => {
this.ensureFactoryExists(embeddableFactoryId);
const factory = this.embeddableFactories.get(embeddableFactoryId);
if (!factory) {
@ -88,6 +126,24 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
);
}
return factory as EmbeddableFactory<I, O>;
return factory as EmbeddableFactory<I, O, E>;
};
// These two functions are only to support legacy plugins registering factories after the start lifecycle.
private ensureFactoriesExist() {
this.embeddableFactoryDefinitions.forEach(def => this.ensureFactoryExists(def.type));
}
private ensureFactoryExists(type: string) {
if (!this.embeddableFactories.get(type)) {
const def = this.embeddableFactoryDefinitions.get(type);
if (!def) return;
this.embeddableFactories.set(
type,
this.customEmbeddableFactoryProvider
? this.customEmbeddableFactoryProvider(def)
: defaultEmbeddableFactoryProvider(def)
);
}
}
}

View file

@ -36,13 +36,13 @@ import { esFilters } from '../../../../plugins/data/public';
test('ApplyFilterAction applies the filter to the root of the container tree', async () => {
const { doStart, setup } = testPlugin();
const api = doStart();
const factory1 = new FilterableContainerFactory(api.getEmbeddableFactory);
const factory2 = new FilterableEmbeddableFactory();
setup.registerEmbeddableFactory(factory1.type, factory1);
const factory1 = new FilterableContainerFactory(async () => await api.getEmbeddableFactory);
setup.registerEmbeddableFactory(factory2.type, factory2);
setup.registerEmbeddableFactory(factory1.type, factory1);
const api = doStart();
const applyFilterAction = createFilterAction();
@ -63,7 +63,9 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
FilterableContainer
>(FILTERABLE_CONTAINER, { panels: {}, id: 'Node2' });
if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) throw new Error();
if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) {
throw new Error();
}
const embeddable = await node2.addNewEmbeddable<
FilterableEmbeddableInput,
@ -94,9 +96,11 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => {
const { doStart, coreStart, setup } = testPlugin();
const api = doStart();
const inspector = inspectorPluginMock.createStartContract();
const factory = new FilterableEmbeddableFactory();
setup.registerEmbeddableFactory(factory.type, factory);
const api = doStart();
const applyFilterAction = createFilterAction();
const parent = new HelloWorldContainer(
{ id: 'root', panels: {} },
@ -110,10 +114,6 @@ test('ApplyFilterAction is incompatible if the root container does not accept a
SavedObjectFinder: () => null,
}
);
const factory = new FilterableEmbeddableFactory();
setup.registerEmbeddableFactory(factory.type, factory);
const embeddable = await parent.addNewEmbeddable<
FilterableContainerInput,
EmbeddableOutput,
@ -130,12 +130,12 @@ test('ApplyFilterAction is incompatible if the root container does not accept a
test('trying to execute on incompatible context throws an error ', async () => {
const { doStart, coreStart, setup } = testPlugin();
const api = doStart();
const inspector = inspectorPluginMock.createStartContract();
const factory = new FilterableEmbeddableFactory();
setup.registerEmbeddableFactory(factory.type, factory);
const api = doStart();
const applyFilterAction = createFilterAction();
const parent = new HelloWorldContainer(
{ id: 'root', panels: {} },

View file

@ -56,8 +56,6 @@ async function creatHelloWorldContainerAndEmbeddable(
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart);
const start = doStart();
const filterableFactory = new FilterableEmbeddableFactory();
const slowContactCardFactory = new SlowContactCardEmbeddableFactory({
execAction: uiActions.executeTriggerActions,
@ -68,6 +66,8 @@ async function creatHelloWorldContainerAndEmbeddable(
setup.registerEmbeddableFactory(slowContactCardFactory.type, slowContactCardFactory);
setup.registerEmbeddableFactory(helloWorldFactory.type, helloWorldFactory);
const start = doStart();
const container = new HelloWorldContainer(containerInput, {
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
@ -563,6 +563,13 @@ test('Container changes made directly after adding a new embeddable are propagat
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart);
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
const container = new HelloWorldContainer(
@ -582,12 +589,6 @@ test('Container changes made directly after adding a new embeddable are propagat
}
);
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
const subscription = Rx.merge(container.getOutput$(), container.getInput$())
.pipe(skip(2))
.subscribe(() => {
@ -759,12 +760,13 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem
coreMock.createSetup(),
coreMock.createStart()
);
const start = doStart();
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',
@ -799,12 +801,12 @@ test('adding a panel then subsequently removing it before its loaded removes the
coreMock.createSetup(),
coreMock.createStart()
);
const start = doStart();
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 1,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',

View file

@ -47,15 +47,14 @@ beforeEach(async () => {
coreMock.createSetup(),
coreMock.createStart()
);
api = doStart();
const contactCardFactory = new ContactCardEmbeddableFactory(
{},
uiActions.executeTriggerActions,
{} as any
);
setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory);
api = doStart();
container = new HelloWorldContainer(
{ id: '123', panels: {} },
{

View file

@ -41,7 +41,6 @@ const { setup, doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
const start = doStart();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const factory = new SlowContactCardEmbeddableFactory({
@ -51,6 +50,8 @@ const factory = new SlowContactCardEmbeddableFactory({
setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, factory);
setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
const start = doStart();
test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const derivedFilter: Filter = {
$state: { store: esFilters.FilterStateStore.APP_STATE },

View file

@ -35,16 +35,16 @@ test('returns empty list if there are no embeddable factories', () => {
test('returns existing embeddable factories', () => {
const { setup, doStart } = testPlugin();
const start = doStart();
const { length } = [...start.getEmbeddableFactories()];
const factory1 = new FilterableContainerFactory(start.getEmbeddableFactory);
const factory2 = new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any);
const factory1 = new FilterableContainerFactory(async () => await start.getEmbeddableFactory);
const factory2 = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
setup.registerEmbeddableFactory(factory1.type, factory1);
setup.registerEmbeddableFactory(factory2.type, factory2);
const start = doStart();
const list = [...start.getEmbeddableFactories()];
expect(list.length - length).toBe(2);
expect(list.length).toBe(2);
expect(!!list.find(({ type }) => factory1.type === type)).toBe(true);
expect(!!list.find(({ type }) => factory2.type === type)).toBe(true);
});

View file

@ -17,6 +17,22 @@
* under the License.
*/
import { EmbeddableFactory } from './lib/embeddables';
import { SavedObjectAttributes } from 'kibana/public';
import {
EmbeddableFactory,
EmbeddableInput,
EmbeddableOutput,
IEmbeddable,
EmbeddableFactoryDefinition,
} from './lib/embeddables';
export type EmbeddableFactoryRegistry = Map<string, EmbeddableFactory>;
export type EmbeddableFactoryProvider = <
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>,
T extends SavedObjectAttributes = SavedObjectAttributes
>(
def: EmbeddableFactoryDefinition<I, O, E, T>
) => EmbeddableFactory<I, O, E, T>;

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Vis } from '../types';
import { VisualizeInput, VisualizeEmbeddable } from './visualize_embeddable';
import { IContainer, ErrorEmbeddable } from '../../../../plugins/embeddable/public';
import { DisabledLabEmbeddable } from './disabled_lab_embeddable';
import {
getSavedVisualizationsLoader,
getUISettings,
getHttp,
getTimeFilter,
getCapabilities,
} from '../services';
export const createVisEmbeddableFromObject = async (
vis: Vis,
input: Partial<VisualizeInput> & { id: string },
parent?: IContainer
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> => {
const savedVisualizations = getSavedVisualizationsLoader();
try {
const visId = vis.id as string;
const editUrl = visId
? getHttp().basePath.prepend(`/app/kibana${savedVisualizations.urlFor(visId)}`)
: '';
const isLabsEnabled = getUISettings().get<boolean>('visualize:enableLabs');
if (!isLabsEnabled && vis.type.stage === 'experimental') {
return new DisabledLabEmbeddable(vis.title, input);
}
const indexPattern = vis.data.indexPattern;
const indexPatterns = indexPattern ? [indexPattern] : [];
const editable = getCapabilities().visualize.save as boolean;
return new VisualizeEmbeddable(
getTimeFilter(),
{
vis,
indexPatterns,
editUrl,
editable,
},
input,
parent
);
} catch (e) {
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);
}
};

View file

@ -21,3 +21,4 @@ export { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable';
export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory';
export { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
export { VIS_EVENT_TO_TRIGGER } from './events';
export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object';

View file

@ -33,8 +33,8 @@ import {
EmbeddableInput,
EmbeddableOutput,
Embeddable,
Container,
EmbeddableVisTriggerContext,
IContainer,
} from '../../../../plugins/embeddable/public';
import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public';
import { IExpressionLoaderParams, ExpressionsStart } from '../../../../plugins/expressions/public';
@ -89,7 +89,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
timefilter: TimefilterContract,
{ vis, editUrl, indexPatterns, editable }: VisualizeEmbeddableConfiguration,
initialInput: VisualizeInput,
parent?: Container
parent?: IContainer
) {
super(
initialInput,
@ -116,7 +116,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
})
);
}
public getVisualizationDescription() {
return this.vis.description;
}

View file

@ -18,70 +18,68 @@
*/
import { i18n } from '@kbn/i18n';
import { SavedObjectMetaData } from 'src/plugins/saved_objects/public';
import { SavedObjectAttributes } from '../../../../core/public';
import {
Container,
EmbeddableFactory,
EmbeddableFactoryDefinition,
EmbeddableOutput,
ErrorEmbeddable,
IContainer,
} from '../../../../plugins/embeddable/public';
import { DisabledLabEmbeddable } from './disabled_lab_embeddable';
import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable';
import { Vis } from '../types';
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
import { Vis } from '../vis';
import {
getCapabilities,
getHttp,
getTypes,
getUISettings,
getSavedVisualizationsLoader,
getTimeFilter,
} from '../services';
import { showNewVisModal } from '../wizard';
import { convertToSerializedVis } from '../saved_visualizations/_saved_vis';
import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object';
interface VisualizationAttributes extends SavedObjectAttributes {
visState: string;
}
export class VisualizeEmbeddableFactory extends EmbeddableFactory<
VisualizeInput,
VisualizeOutput | EmbeddableOutput,
VisualizeEmbeddable | DisabledLabEmbeddable,
VisualizationAttributes
> {
export class VisualizeEmbeddableFactory
implements
EmbeddableFactoryDefinition<
VisualizeInput,
VisualizeOutput | EmbeddableOutput,
VisualizeEmbeddable | DisabledLabEmbeddable,
VisualizationAttributes
> {
public readonly type = VISUALIZE_EMBEDDABLE_TYPE;
constructor() {
super({
savedObjectMetaData: {
name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }),
includeFields: ['visState'],
type: 'visualization',
getIconForSavedObject: savedObject => {
return (
getTypes().get(JSON.parse(savedObject.attributes.visState).type).icon || 'visualizeApp'
);
},
getTooltipForSavedObject: savedObject => {
return `${savedObject.attributes.title} (${
getTypes().get(JSON.parse(savedObject.attributes.visState).type).title
})`;
},
showSavedObject: savedObject => {
const typeName: string = JSON.parse(savedObject.attributes.visState).type;
const visType = getTypes().get(typeName);
if (!visType) {
return false;
}
if (getUISettings().get('visualize:enableLabs')) {
return true;
}
return visType.stage !== 'experimental';
},
},
});
}
public readonly savedObjectMetaData: SavedObjectMetaData<VisualizationAttributes> = {
name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }),
includeFields: ['visState'],
type: 'visualization',
getIconForSavedObject: savedObject => {
return (
getTypes().get(JSON.parse(savedObject.attributes.visState).type).icon || 'visualizeApp'
);
},
getTooltipForSavedObject: savedObject => {
return `${savedObject.attributes.title} (${
getTypes().get(JSON.parse(savedObject.attributes.visState).type).title
})`;
},
showSavedObject: savedObject => {
const typeName: string = JSON.parse(savedObject.attributes.visState).type;
const visType = getTypes().get(typeName);
if (!visType) {
return false;
}
if (getUISettings().get('visualize:enableLabs')) {
return true;
}
return visType.stage !== 'experimental';
},
};
constructor() {}
public async isEditable() {
return getCapabilities().visualize.save as boolean;
@ -93,56 +91,17 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
});
}
public async createFromObject(
vis: Vis,
input: Partial<VisualizeInput> & { id: string },
parent?: Container
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> {
const savedVisualizations = getSavedVisualizationsLoader();
try {
const visId = vis.id as string;
const editUrl = visId
? getHttp().basePath.prepend(`/app/kibana${savedVisualizations.urlFor(visId)}`)
: '';
const isLabsEnabled = getUISettings().get<boolean>('visualize:enableLabs');
if (!isLabsEnabled && vis.type.stage === 'experimental') {
return new DisabledLabEmbeddable(vis.title, input);
}
const indexPattern = vis.data.indexPattern;
const indexPatterns = indexPattern ? [indexPattern] : [];
const editable = await this.isEditable();
return new VisualizeEmbeddable(
getTimeFilter(),
{
vis,
indexPatterns,
editUrl,
editable,
},
input,
parent
);
} catch (e) {
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);
}
}
public async createFromSavedObject(
savedObjectId: string,
input: Partial<VisualizeInput> & { id: string },
parent?: Container
parent?: IContainer
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> {
const savedVisualizations = getSavedVisualizationsLoader();
try {
const savedObject = await savedVisualizations.get(savedObjectId);
const vis = new Vis(savedObject.visState.type, await convertToSerializedVis(savedObject));
return this.createFromObject(vis, input, parent);
return createVisEmbeddableFromObject(vis, input, parent);
} catch (e) {
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);

View file

@ -43,6 +43,9 @@ const createStartContract = (): VisualizationsStart => ({
createVis: jest.fn(),
convertFromSerializedVis: jest.fn(),
convertToSerializedVis: jest.fn(),
__LEGACY: {
createVisEmbeddableFromObject: jest.fn(),
},
});
const createInstance = async () => {

View file

@ -37,7 +37,11 @@ import {
setChrome,
setOverlays,
} from './services';
import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable';
import {
VISUALIZE_EMBEDDABLE_TYPE,
VisualizeEmbeddableFactory,
createVisEmbeddableFromObject,
} from './embeddable';
import { ExpressionsSetup, ExpressionsStart } from '../../../plugins/expressions/public';
import { EmbeddableSetup } from '../../../plugins/embeddable/public';
import { visualization as visualizationFunction } from './expressions/visualization_function';
@ -69,6 +73,7 @@ export interface VisualizationsStart extends TypesStart {
convertToSerializedVis: typeof convertToSerializedVis;
convertFromSerializedVis: typeof convertFromSerializedVis;
showNewVisModal: typeof showNewVisModal;
__LEGACY: { createVisEmbeddableFromObject: typeof createVisEmbeddableFromObject };
}
export interface VisualizationsSetupDeps {
@ -163,6 +168,7 @@ export class VisualizationsPlugin
convertToSerializedVis,
convertFromSerializedVis,
savedVisualizationsLoader,
__LEGACY: { createVisEmbeddableFromObject },
};
}

View file

@ -29,7 +29,6 @@ import {
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
DashboardContainerFactory,
DashboardContainerInput,
} from '../../../../../../../../src/plugins/dashboard/public';
@ -70,8 +69,9 @@ export class DashboardContainerExample extends React.Component<Props, State> {
this.mounted = true;
const dashboardFactory = this.props.getEmbeddableFactory<
DashboardContainerInput,
ContainerOutput
>(DASHBOARD_CONTAINER_TYPE) as DashboardContainerFactory;
ContainerOutput,
DashboardContainer
>(DASHBOARD_CONTAINER_TYPE);
if (dashboardFactory) {
this.container = await dashboardFactory.create(dashboardInput);
if (this.mounted) {

View file

@ -18,7 +18,7 @@ import {
} from '../../../../../../../src/plugins/data/public';
import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public';
import {
EmbeddableFactory as AbstractEmbeddableFactory,
EmbeddableFactoryDefinition,
ErrorEmbeddable,
EmbeddableInput,
IContainer,
@ -36,25 +36,22 @@ interface StartServices {
indexPatternService: IndexPatternsContract;
}
export class EmbeddableFactory extends AbstractEmbeddableFactory {
export class EmbeddableFactory implements EmbeddableFactoryDefinition {
type = DOC_TYPE;
savedObjectMetaData = {
name: i18n.translate('xpack.lens.lensSavedObjectLabel', {
defaultMessage: 'Lens Visualization',
}),
type: DOC_TYPE,
getIconForSavedObject: () => 'lensApp',
};
constructor(private getStartServices: () => Promise<StartServices>) {
super({
savedObjectMetaData: {
name: i18n.translate('xpack.lens.lensSavedObjectLabel', {
defaultMessage: 'Lens Visualization',
}),
type: DOC_TYPE,
getIconForSavedObject: () => 'lensApp',
},
});
}
constructor(private getStartServices: () => Promise<StartServices>) {}
public async isEditable() {
public isEditable = async () => {
const { capabilities } = await this.getStartServices();
return capabilities.visualize.save as boolean;
}
};
canCreateNew() {
return false;
@ -66,11 +63,11 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory {
});
}
async createFromSavedObject(
createFromSavedObject = async (
savedObjectId: string,
input: Partial<EmbeddableInput> & { id: string },
parent?: IContainer
) {
) => {
const {
savedObjectsClient,
coreHttp,
@ -111,7 +108,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory {
input,
parent
);
}
};
async create(input: EmbeddableInput) {
return new ErrorEmbeddable('Lens can only be created from a saved object', input);

View file

@ -30,17 +30,15 @@
### Creating a Map embeddable from state
```
const factory = new MapEmbeddableFactory();
const state = {
layerList: [], // where layerList is same as saved object layerListJSON property (unstringified)
title: 'my map',
}
const input = {
hideFilterActions: true,
isLayerTOCOpen: false,
openTOCDetails: ['tfi3f', 'edh66'],
mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 }
}
const mapEmbeddable = await factory.createFromState(state, input, parent);
const mapEmbeddable = await factory.create(input, parent);
// where layerList is same as saved object layerListJSON property (unstringified))
mapEmbeddable.setLayerList([]);
```
#### Customize tooltip
@ -62,7 +60,9 @@ const renderTooltipContent = ({ addFilters, closeTooltip, features, isLocked, lo
return <div>Custom tooltip content</div>;
}
const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent);
const mapEmbeddable = await factory.create(input, parent)
mapEmbeddable.setLayerList(layerList);
mapEmbeddable.setRenderTooltipContent(renderTooltipContent);
```
@ -80,7 +80,10 @@ const eventHandlers = {
},
}
const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent, eventHandlers);
const mapEmbeddable = await factory.create(input, parent);
mapEmbeddable.setLayerList(layerList);
mapEmbeddable.setRenderTooltipContent(renderTooltipContent);
mapEmbeddable.setEventHandlers(eventHandlers);
```
@ -90,55 +93,13 @@ Geojson sources will not update unless you modify `__featureCollection` property
```
const factory = new MapEmbeddableFactory();
const state = {
layerList: [
{
'id': 'gaxya',
'label': 'My geospatial data',
'minZoom': 0,
'maxZoom': 24,
'alpha': 1,
'sourceDescriptor': {
'id': 'b7486',
'type': 'GEOJSON_FILE',
'__featureCollection': {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[0, 0], [10, 10], [10, 0], [0, 0]
]
]
},
"properties": {
"name": "null island",
"another_prop": "something else interesting"
}
}
]
}
},
'visible': true,
'style': {
'type': 'VECTOR',
'properties': {}
},
'type': 'VECTOR'
}
],
title: 'my map',
}
const input = {
hideFilterActions: true,
isLayerTOCOpen: false,
openTOCDetails: ['tfi3f', 'edh66'],
mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 }
}
const mapEmbeddable = await factory.createFromState(state, input, parent);
const mapEmbeddable = await factory.create(input, parent);
mapEmbeddable.setLayerList([
{

View file

@ -129,6 +129,14 @@ export class MapEmbeddable extends Embeddable<MapEmbeddableInput, MapEmbeddableO
this._subscription = this.getInput$().subscribe(input => this.onContainerStateChanged(input));
}
setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => {
this._renderTooltipContent = renderTooltipContent;
};
setEventHandlers = (eventHandlers: EventHandlers) => {
this._eventHandlers = eventHandlers;
};
getInspectorAdapters() {
return getInspectorAdapters(this._store.getState());
}

View file

@ -14,8 +14,7 @@ import { IIndexPattern } from 'src/plugins/data/public';
import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable';
import { getIndexPatternService } from '../kibana_services';
import {
EmbeddableFactory,
ErrorEmbeddable,
EmbeddableFactoryDefinition,
IContainer,
} from '../../../../../../src/plugins/embeddable/public';
@ -28,25 +27,17 @@ import { getInitialLayers } from '../angular/get_initial_layers';
import { mergeInputWithSavedMap } from './merge_input_with_saved_map';
import '../angular/services/gis_map_saved_object_loader';
import { bindSetupCoreAndPlugins, bindStartCoreAndPlugins } from '../plugin';
import { RenderToolTipContent } from '../layers/tooltips/tooltip_property';
import {
EventHandlers,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../plugins/maps/public/reducers/non_serializable_instances';
export class MapEmbeddableFactory extends EmbeddableFactory {
export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
type = MAP_SAVED_OBJECT_TYPE;
savedObjectMetaData = {
name: i18n.translate('xpack.maps.mapSavedObjectLabel', {
defaultMessage: 'Map',
}),
type: MAP_SAVED_OBJECT_TYPE,
getIconForSavedObject: () => APP_ICON,
};
constructor() {
super({
savedObjectMetaData: {
name: i18n.translate('xpack.maps.mapSavedObjectLabel', {
defaultMessage: 'Map',
}),
type: MAP_SAVED_OBJECT_TYPE,
getIconForSavedObject: () => APP_ICON,
},
});
// Init required services. Necessary while in legacy
bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins);
bindStartCoreAndPlugins(npStart.core, npStart.plugins);
@ -103,11 +94,11 @@ export class MapEmbeddableFactory extends EmbeddableFactory {
return await savedObjectLoader.get(savedObjectId);
}
async createFromSavedObject(
createFromSavedObject = async (
savedObjectId: string,
input: MapEmbeddableInput,
parent?: IContainer
) {
) => {
const savedMap = await this._fetchSavedMap(savedObjectId);
const layerList = getInitialLayers(savedMap.layerListJSON);
const indexPatterns = await this._getIndexPatterns(layerList);
@ -135,39 +126,23 @@ export class MapEmbeddableFactory extends EmbeddableFactory {
}
return embeddable;
}
};
async createFromState(
state: { title?: string; layerList?: unknown[] },
input: MapEmbeddableInput,
parent: IContainer,
renderTooltipContent: RenderToolTipContent,
eventHandlers: EventHandlers
) {
const layerList = state && state.layerList ? state.layerList : getInitialLayers();
create = async (input: MapEmbeddableInput, parent?: IContainer) => {
const layerList = getInitialLayers();
const indexPatterns = await this._getIndexPatterns(layerList);
return new MapEmbeddable(
{
layerList,
title: state && state.title ? state.title : '',
title: input.title ?? '',
indexPatterns,
editable: false,
},
input,
parent,
renderTooltipContent,
eventHandlers
parent
);
}
async create(input: MapEmbeddableInput) {
window.location.href = chrome.addBasePath(createMapPath(''));
return new ErrorEmbeddable(
'Maps can only be created with createFromSavedObject or createFromState',
input
);
}
};
}
npSetup.plugins.embeddable.registerEmbeddableFactory(

View file

@ -10,7 +10,10 @@ import { createPortalNode, InPortal } from 'react-reverse-portal';
import styled, { css } from 'styled-components';
import { npStart } from 'ui/new_platform';
import { EmbeddablePanel } from '../../../../../../../src/plugins/embeddable/public';
import {
EmbeddablePanel,
ErrorEmbeddable,
} from '../../../../../../../src/plugins/embeddable/public';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers';
import { useIndexPatterns } from '../../hooks/use_index_patterns';
@ -84,7 +87,9 @@ export const EmbeddedMapComponent = ({
setQuery,
startDate,
}: EmbeddedMapProps) => {
const [embeddable, setEmbeddable] = React.useState<MapEmbeddable | null>(null);
const [embeddable, setEmbeddable] = React.useState<MapEmbeddable | undefined | ErrorEmbeddable>(
undefined
);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [isIndexError, setIsIndexError] = useState(false);

View file

@ -20,8 +20,10 @@ jest.mock('ui/new_platform');
const { npStart } = createUiNewPlatformMock();
npStart.plugins.embeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({
createFromState: () => ({
create: () => ({
reload: jest.fn(),
setRenderTooltipContent: jest.fn(),
setLayerList: jest.fn(),
}),
}));

View file

@ -11,10 +11,20 @@ import minimatch from 'minimatch';
import { IndexPatternMapping, SetQuery } from './types';
import { getLayerList } from './map_config';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../plugins/maps/public';
import { MapEmbeddable, RenderTooltipContentParams } from '../../../../maps/public';
import {
MapEmbeddable,
RenderTooltipContentParams,
MapEmbeddableInput,
} from '../../../../maps/public';
import * as i18n from './translations';
import { Query, Filter } from '../../../../../../../src/plugins/data/public';
import { EmbeddableStart, ViewMode } from '../../../../../../../src/plugins/embeddable/public';
import {
EmbeddableStart,
isErrorEmbeddable,
EmbeddableOutput,
ViewMode,
ErrorEmbeddable,
} from '../../../../../../../src/plugins/embeddable/public';
import { IndexPatternSavedObject } from '../../hooks/types';
/**
@ -40,14 +50,19 @@ export const createEmbeddable = async (
setQuery: SetQuery,
portalNode: PortalNode,
embeddableApi: EmbeddableStart
): Promise<MapEmbeddable> => {
const factory = embeddableApi.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);
): Promise<MapEmbeddable | ErrorEmbeddable> => {
const factory = embeddableApi.getEmbeddableFactory<
MapEmbeddableInput,
EmbeddableOutput,
MapEmbeddable
>(MAP_SAVED_OBJECT_TYPE);
const state = {
layerList: getLayerList(indexPatterns),
if (!factory) {
throw new Error('Map embeddable factory undefined');
}
const input: MapEmbeddableInput = {
title: i18n.MAP_TITLE,
};
const input = {
id: uuid.v4(),
filters,
hidePanelTitles: true,
@ -86,13 +101,16 @@ export const createEmbeddable = async (
return <OutPortal node={portalNode} {...props} />;
};
// @ts-ignore method added in https://github.com/elastic/kibana/pull/43878
const embeddableObject = await factory.createFromState(
state,
input,
undefined,
renderTooltipContent
);
const embeddableObject = await factory.create(input);
if (!embeddableObject) {
throw new Error('Map embeddable is undefined');
}
if (!isErrorEmbeddable(embeddableObject)) {
embeddableObject.setRenderTooltipContent(renderTooltipContent);
embeddableObject.setLayerList(getLayerList(indexPatterns));
}
// Wire up to app refresh action
setQuery({

View file

@ -9,7 +9,12 @@ import uuid from 'uuid';
import styled from 'styled-components';
import { npStart } from 'ui/new_platform';
import { ViewMode } from '../../../../../../../../../src/plugins/embeddable/public';
import {
ViewMode,
EmbeddableOutput,
ErrorEmbeddable,
isErrorEmbeddable,
} from '../../../../../../../../../src/plugins/embeddable/public';
import * as i18n from './translations';
import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../maps/public';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../../plugins/maps/public';
@ -45,9 +50,13 @@ const EmbeddedPanel = styled.div`
export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => {
const { colors } = useContext(UptimeThemeContext);
const [embeddable, setEmbeddable] = useState<MapEmbeddable>();
const [embeddable, setEmbeddable] = useState<MapEmbeddable | ErrorEmbeddable | undefined>();
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
const factory = npStart.plugins.embeddable.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE);
const factory = npStart.plugins.embeddable.getEmbeddableFactory<
MapEmbeddableInput,
EmbeddableOutput,
MapEmbeddable
>(MAP_SAVED_OBJECT_TYPE);
const input: MapEmbeddableInput = {
id: uuid.v4(),
@ -76,12 +85,17 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
useEffect(() => {
async function setupEmbeddable() {
const mapState = {
layerList: getLayerList(upPoints, downPoints, colors),
if (!factory) {
throw new Error('Map embeddable not found.');
}
const embeddableObject = await factory.create({
...input,
title: i18n.MAP_TITLE,
};
// @ts-ignore
const embeddableObject = await factory.createFromState(mapState, input, undefined);
});
if (embeddableObject && !isErrorEmbeddable(embeddableObject)) {
embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors));
}
setEmbeddable(embeddableObject);
}
@ -93,7 +107,7 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp
// update map layers based on points
useEffect(() => {
if (embeddable) {
if (embeddable && !isErrorEmbeddable(embeddable)) {
embeddable.setLayerList(getLayerList(upPoints, downPoints, colors));
}
}, [upPoints, downPoints, embeddable, colors]);

View file

@ -10,9 +10,7 @@ import { skip } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeAction } from './custom_time_range_action';
/* eslint-disable */
import {
@ -21,7 +19,6 @@ import {
/* eslint-enable */
import {
HelloWorldEmbeddableFactory,
HelloWorldEmbeddable,
HELLO_WORLD_EMBEDDABLE,
} from '../../../../examples/embeddable_examples/public';
@ -38,9 +35,6 @@ const createOpenModalMock = () => {
};
test('Custom time range action prevents embeddable from using container time', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@ -105,9 +99,6 @@ test('Custom time range action prevents embeddable from using container time', a
});
test('Removing custom time range action resets embeddable back to container time', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@ -182,9 +173,6 @@ test('Removing custom time range action resets embeddable back to container time
});
test('Cancelling custom time range action leaves state alone', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@ -244,8 +232,6 @@ test('Cancelling custom time range action leaves state alone', async done => {
});
test(`badge is compatible with embeddable that inherits from parent`, async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@ -279,8 +265,6 @@ test(`badge is compatible with embeddable that inherits from parent`, async () =
// TODO: uncomment when https://github.com/elastic/kibana/issues/43271 is fixed.
// test('Embeddable that does not use time range in a container that has time range is incompatible', async () => {
// const embeddableFactories = new Map<string, EmbeddableFactory>();
// embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
// const container = new TimeRangeContainer(
// {
// timeRange: { from: 'now-15m', to: 'now' },
@ -315,8 +299,6 @@ test(`badge is compatible with embeddable that inherits from parent`, async () =
// });
test('Attempting to execute on incompatible embeddable throws an error', async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory());
const container = new HelloWorldContainer(
{
panels: {

View file

@ -9,17 +9,12 @@ import { findTestSubject } from '@elastic/eui/lib/test';
import { skip } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
import { ReactElement } from 'react';
import { nextTick } from 'test_utils/enzyme_helpers';
test('Removing custom time range from badge resets embeddable back to container time', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@ -79,8 +74,6 @@ test('Removing custom time range from badge resets embeddable back to container
});
test(`badge is not compatible with embeddable that inherits from parent`, async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@ -113,8 +106,6 @@ test(`badge is not compatible with embeddable that inherits from parent`, async
});
test(`badge is compatible with embeddable that has custom time range`, async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
@ -148,8 +139,6 @@ test(`badge is compatible with embeddable that has custom time range`, async ()
});
test('Attempting to execute on incompatible embeddable throws an error', async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },

View file

@ -7,7 +7,7 @@
import {
EmbeddableInput,
IContainer,
EmbeddableFactory,
EmbeddableFactoryDefinition,
} from '../../../../../src/plugins/embeddable/public';
import { TimeRange } from '../../../../../src/plugins/data/public';
import { TIME_RANGE_EMBEDDABLE, TimeRangeEmbeddable } from './time_range_embeddable';
@ -16,7 +16,8 @@ interface EmbeddableTimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
export class TimeRangeEmbeddableFactory extends EmbeddableFactory<EmbeddableTimeRangeInput> {
export class TimeRangeEmbeddableFactory
implements EmbeddableFactoryDefinition<EmbeddableTimeRangeInput> {
public readonly type = TIME_RANGE_EMBEDDABLE;
public async isEditable() {

View file

@ -6,13 +6,13 @@
import { i18n } from '@kbn/i18n';
import {
EmbeddableFactory,
IContainer,
EmbeddableInput,
EmbeddableFactoryDefinition,
} from '../../../../../../src/plugins/embeddable/public';
import { ResolverEmbeddable } from './embeddable';
export class ResolverEmbeddableFactory extends EmbeddableFactory {
export class ResolverEmbeddableFactory implements EmbeddableFactoryDefinition {
public readonly type = 'resolver';
public async isEditable() {