[embeddable] remove legacy embeddable test samples (#203678)

Remove legacy embeddable test constructs. PR removes tests that use test
samples and some code that could be easily removed.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-12-11 09:27:03 -07:00 committed by GitHub
parent 59d3ac65ec
commit c59dddfa45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 14 additions and 3101 deletions

View file

@ -10,7 +10,6 @@
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
import { DashboardGrid } from './dashboard_grid';
import { buildMockDashboardApi } from '../../../mocks';
@ -50,12 +49,12 @@ jest.mock('./dashboard_grid_item', () => {
const PANELS = {
'1': {
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
type: CONTACT_CARD_EMBEDDABLE,
type: 'lens',
explicitInput: { id: '1' },
},
'2': {
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
type: CONTACT_CARD_EMBEDDABLE,
type: 'lens',
explicitInput: { id: '2' },
},
};

View file

@ -1,125 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { of } from 'rxjs';
import { embeddablePluginMock } from '../../../mocks';
import { applicationServiceMock } from '@kbn/core/public/mocks';
import { Embeddable, EmbeddableInput, ViewMode } from '../..';
import { canEditEmbeddable, editLegacyEmbeddable } from './edit_legacy_embeddable';
import { ContactCardEmbeddable } from '../../test_samples';
import { core, embeddableStart } from '../../../kibana_services';
const applicationMock = applicationServiceMock.createStartContract();
const stateTransferMock = embeddablePluginMock.createStartContract().getStateTransfer();
// mock app id
core.application.currentAppId$ = of('superCoolCurrentApp');
class EditableEmbeddable extends Embeddable {
public readonly type = 'EDITABLE_EMBEDDABLE';
constructor(input: EmbeddableInput, editable: boolean) {
super(input, {
editUrl: 'www.google.com',
editable,
});
}
public reload() {}
}
test('canEditEmbeddable returns true when edit url is available, in edit mode and editable', () => {
expect(
canEditEmbeddable(new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true))
).toBe(true);
});
test('canEditEmbeddable returns false when edit url is not available', async () => {
const embeddable = new ContactCardEmbeddable(
{
id: '123',
firstName: 'sue',
viewMode: ViewMode.EDIT,
},
{
execAction: () => Promise.resolve(undefined),
}
);
expect(canEditEmbeddable(embeddable)).toBe(false);
});
test('canEditEmbeddable returns false when edit url is available but in view mode', async () => {
expect(
canEditEmbeddable(
new EditableEmbeddable(
{
id: '123',
viewMode: ViewMode.VIEW,
},
true
)
)
).toBe(false);
});
test('canEditEmbeddable returns false when edit url is available, in edit mode, but not editable', async () => {
expect(
canEditEmbeddable(
new EditableEmbeddable(
{
id: '123',
viewMode: ViewMode.EDIT,
},
false
)
)
).toBe(false);
});
test('getEditHref returns the edit url', async () => {
const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true);
expect(await embeddable.getEditHref()).toBe(embeddable.getOutput().editUrl);
});
test('redirects to app using state transfer', async () => {
embeddableStart.getStateTransfer = jest.fn().mockReturnValue(stateTransferMock);
applicationMock.currentAppId$ = of('superCoolCurrentApp');
const testPath = '/test-path';
const embeddable = new EditableEmbeddable(
{
id: '123',
viewMode: ViewMode.EDIT,
coolInput1: 1,
coolInput2: 2,
} as unknown as EmbeddableInput,
true
);
embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' }));
embeddable.getAppContext = jest.fn().mockReturnValue({
getCurrentPath: () => testPath,
});
await editLegacyEmbeddable(embeddable);
expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', {
path: '/123',
state: {
originatingApp: 'superCoolCurrentApp',
embeddableId: '123',
valueInput: {
id: '123',
viewMode: ViewMode.EDIT,
coolInput1: 1,
coolInput2: 2,
},
originatingPath: testPath,
searchSessionId: undefined,
},
});
});

View file

@ -1,99 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { firstValueFrom } from 'rxjs';
import { LegacyCompatibleEmbeddable } from '../../../embeddable_panel/types';
import { core, embeddableStart } from '../../../kibana_services';
import { Container } from '../../containers';
import { EmbeddableFactoryNotFoundError } from '../../errors';
import { EmbeddableEditorState } from '../../state_transfer';
import { isExplicitInputWithAttributes } from '../embeddable_factory';
import { EmbeddableInput } from '../i_embeddable';
const getExplicitInput = (embeddable: LegacyCompatibleEmbeddable) =>
(embeddable.getRoot() as Container)?.getInput()?.panels?.[embeddable.id]?.explicitInput ??
embeddable.getInput();
const getAppTarget = async (embeddable: LegacyCompatibleEmbeddable) => {
const app = embeddable ? embeddable.getOutput().editApp : undefined;
const path = embeddable ? embeddable.getOutput().editPath : undefined;
if (!app || !path) return;
const currentAppId = await firstValueFrom(core.application.currentAppId$);
if (!currentAppId) return { app, path };
const state: EmbeddableEditorState = {
originatingApp: currentAppId,
valueInput: getExplicitInput(embeddable),
embeddableId: embeddable.id,
searchSessionId: embeddable.getInput().searchSessionId,
originatingPath: embeddable.getAppContext()?.getCurrentPath?.(),
};
return { app, path, state };
};
export const editLegacyEmbeddable = async (embeddable: LegacyCompatibleEmbeddable) => {
const { editableWithExplicitInput } = embeddable.getOutput();
if (editableWithExplicitInput) {
const factory = embeddableStart.getEmbeddableFactory(embeddable.type);
if (!factory) {
throw new EmbeddableFactoryNotFoundError(embeddable.type);
}
const oldExplicitInput = embeddable.getExplicitInput();
let newExplicitInput: Partial<EmbeddableInput>;
try {
const explicitInputReturn = await factory.getExplicitInput(
oldExplicitInput,
embeddable.parent
);
newExplicitInput = isExplicitInputWithAttributes(explicitInputReturn)
? explicitInputReturn.newInput
: explicitInputReturn;
} catch (e) {
// error likely means user canceled editing
return;
}
embeddable.parent?.replaceEmbeddable(embeddable.id, newExplicitInput);
return;
}
const appTarget = await getAppTarget(embeddable);
const stateTransfer = embeddableStart.getStateTransfer();
if (appTarget) {
if (stateTransfer && appTarget.state) {
await stateTransfer.navigateToEditor(appTarget.app, {
path: appTarget.path,
state: appTarget.state,
});
} else {
await core.application.navigateToApp(appTarget.app, { path: appTarget.path });
}
return;
}
const href = embeddable.getOutput().editUrl;
if (href) {
window.location.href = href;
return;
}
};
export const canEditEmbeddable = (embeddable: LegacyCompatibleEmbeddable) => {
return Boolean(
embeddable &&
embeddable.getInput().viewMode === 'edit' &&
embeddable.getOutput().editable &&
!embeddable.getOutput().inlineEditable &&
(embeddable.getOutput().editUrl ||
(embeddable.getOutput().editApp && embeddable.getOutput().editPath) ||
embeddable.getOutput().editableWithExplicitInput)
);
};

View file

@ -37,14 +37,11 @@ import {
IEmbeddable,
LegacyEmbeddableAPI,
} from '../i_embeddable';
import { canEditEmbeddable, editLegacyEmbeddable } from './edit_legacy_embeddable';
import {
embeddableInputToSubject,
embeddableOutputToSubject,
viewModeToSubject,
} from './embeddable_compatibility_utils';
import { canLinkLegacyEmbeddable, linkLegacyEmbeddable } from './link_legacy_embeddable';
import { canUnlinkLegacyEmbeddable, unlinkLegacyEmbeddable } from './unlink_legacy_embeddable';
export type CommonLegacyInput = EmbeddableInput & { savedObjectId?: string; timeRange: TimeRange };
export type CommonLegacyOutput = EmbeddableOutput & { indexPatterns: DataView[] };
@ -93,13 +90,15 @@ export const legacyEmbeddableToApi = (
/**
* Support editing of legacy embeddables
*/
const onEdit = () => editLegacyEmbeddable(embeddable);
const onEdit = () => {
throw new Error('Edit legacy embeddable not supported');
};
const getTypeDisplayName = () =>
embeddableStart.getEmbeddableFactory(embeddable.type)?.getDisplayName() ??
i18n.translate('embeddableApi.compatibility.defaultTypeDisplayName', {
defaultMessage: 'chart',
});
const isEditingEnabled = () => canEditEmbeddable(embeddable);
const isEditingEnabled = () => false;
/**
* Performance tracking
@ -286,11 +285,15 @@ export const legacyEmbeddableToApi = (
panelDescription,
defaultPanelDescription,
canLinkToLibrary: () => canLinkLegacyEmbeddable(embeddable),
linkToLibrary: () => linkLegacyEmbeddable(embeddable),
canLinkToLibrary: async () => false,
linkToLibrary: () => {
throw new Error('Link to library not supported for legacy embeddable');
},
canUnlinkFromLibrary: () => canUnlinkLegacyEmbeddable(embeddable),
unlinkFromLibrary: () => unlinkLegacyEmbeddable(embeddable),
canUnlinkFromLibrary: async () => false,
unlinkFromLibrary: () => {
throw new Error('Unlink from library not supported for legacy embeddable');
},
savedObjectId,
},

View file

@ -1,173 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EmbeddableInput, ErrorEmbeddable, IContainer, SavedObjectEmbeddableInput } from '../..';
import { core } from '../../../kibana_services';
import { embeddablePluginMock } from '../../../mocks';
import { createHelloWorldContainerAndEmbeddable } from '../../../tests/helpers';
import { ReferenceOrValueEmbeddable } from '../../reference_or_value_embeddable';
import {
ContactCardEmbeddable,
ContactCardEmbeddableFactory,
ContactCardEmbeddableInput,
} from '../../test_samples';
import { ViewMode } from '../../types';
import { CommonLegacyEmbeddable } from './legacy_embeddable_to_api';
import { canLinkLegacyEmbeddable, linkLegacyEmbeddable } from './link_legacy_embeddable';
let container: IContainer;
let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable;
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
const defaultCapabilities = {
advancedSettings: {},
visualize: { save: true },
maps: { save: true },
navLinks: {},
};
beforeEach(async () => {
const result = await createHelloWorldContainerAndEmbeddable();
container = result.container;
embeddable = embeddablePluginMock.mockRefOrValEmbeddable<
ContactCardEmbeddable,
ContactCardEmbeddableInput
>(result.embeddable, {
mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: result.embeddable.id },
mockedByValueInput: { firstName: 'Kibanana', id: result.embeddable.id },
});
embeddable.updateInput({ viewMode: ViewMode.EDIT });
});
const assignDefaultCapabilities = () => {
Object.defineProperty(core.application, 'capabilities', {
value: defaultCapabilities,
});
};
test('Cannot link an Error Embeddable to the library', async () => {
assignDefaultCapabilities();
const errorEmbeddable = new ErrorEmbeddable(
'Wow what an awful error',
{ id: ' 404' },
embeddable.getRoot() as IContainer
);
expect(await canLinkLegacyEmbeddable(errorEmbeddable as unknown as CommonLegacyEmbeddable)).toBe(
false
);
});
test('Cannot link an ES|QL Embeddable to the library', async () => {
assignDefaultCapabilities();
const filterableEmbeddable = embeddablePluginMock.mockFilterableEmbeddable(embeddable, {
getFilters: () => [],
getQuery: () => ({
esql: 'from logstash-* | limit 10',
}),
});
expect(
await canLinkLegacyEmbeddable(filterableEmbeddable as unknown as CommonLegacyEmbeddable)
).toBe(false);
});
test('Cannot link a visualize embeddable to the library without visualize save permissions', async () => {
Object.defineProperty(core.application, 'capabilities', {
value: { ...defaultCapabilities, visualize: { save: false } },
});
expect(await canLinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(
false
);
});
test('Can link an embeddable to the library when it has value type input', async () => {
assignDefaultCapabilities();
embeddable.updateInput(await embeddable.getInputAsValueType());
expect(await canLinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(true);
});
test('Cannot link an embedable when its input is by reference', async () => {
assignDefaultCapabilities();
embeddable.updateInput(await embeddable.getInputAsRefType());
expect(await canLinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(
false
);
});
test('Cannot link an embedable when view mode is set to view', async () => {
assignDefaultCapabilities();
embeddable.updateInput(await embeddable.getInputAsRefType());
embeddable.updateInput({ viewMode: ViewMode.VIEW });
expect(await canLinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(
false
);
});
test('Cannot link an embedable when it is not a child of a Dashboard container', async () => {
assignDefaultCapabilities();
let orphanContactCard = await embeddableFactory.create({
id: 'orphanContact',
firstName: 'Orphan',
});
orphanContactCard = embeddablePluginMock.mockRefOrValEmbeddable<
ContactCardEmbeddable,
ContactCardEmbeddableInput
>(orphanContactCard, {
mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id },
mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id },
});
expect(
await canLinkLegacyEmbeddable(orphanContactCard as unknown as CommonLegacyEmbeddable)
).toBe(false);
});
test('Linking an embeddable replaces embeddableId and retains panel count', async () => {
assignDefaultCapabilities();
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelCount = Object.keys(dashboard.getInput().panels).length;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
await linkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable);
expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount);
const newPanelId = Object.keys(container.getInput().panels).find(
(key) => !originalPanelKeySet.has(key)
);
expect(newPanelId).toBeDefined();
const newPanel = container.getInput().panels[newPanelId!];
expect(newPanel.type).toEqual(embeddable.type);
});
test('Link legacy embeddable returns reference type input', async () => {
assignDefaultCapabilities();
const complicatedAttributes = {
attribute1: 'The best attribute',
attribute2: 22,
attribute3: ['array', 'of', 'strings'],
attribute4: { nestedattribute: 'hello from the nest' },
};
embeddable = embeddablePluginMock.mockRefOrValEmbeddable<ContactCardEmbeddable>(embeddable, {
mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id },
mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id } as EmbeddableInput,
});
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
await linkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable);
const newPanelId = Object.keys(container.getInput().panels).find(
(key) => !originalPanelKeySet.has(key)
);
expect(newPanelId).toBeDefined();
const newPanel = container.getInput().panels[newPanelId!];
expect(newPanel.type).toEqual(embeddable.type);
expect((newPanel.explicitInput as unknown as { attributes: unknown }).attributes).toBeUndefined();
expect((newPanel.explicitInput as SavedObjectEmbeddableInput).savedObjectId).toBe(
'testSavedObjectId'
);
});

View file

@ -1,67 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { AggregateQuery } from '@kbn/es-query';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { core } from '../../../kibana_services';
import { PanelNotFoundError } from '../../errors';
import { isFilterableEmbeddable } from '../../filterable_embeddable';
import { isReferenceOrValueEmbeddable } from '../../reference_or_value_embeddable';
import { isErrorEmbeddable } from '../is_error_embeddable';
import { CommonLegacyEmbeddable } from './legacy_embeddable_to_api';
import { IContainer } from '../../containers';
export const canLinkLegacyEmbeddable = async (embeddable: CommonLegacyEmbeddable) => {
// linking and unlinking legacy embeddables is only supported on Dashboard
if (
isErrorEmbeddable(embeddable) ||
!(embeddable.getRoot() && embeddable.getRoot().isContainer) ||
!isReferenceOrValueEmbeddable(embeddable)
) {
return false;
}
const { visualize } = core.application.capabilities;
const canSave = visualize.save;
const { isOfAggregateQueryType } = await import('@kbn/es-query');
const query = isFilterableEmbeddable(embeddable) && embeddable.getQuery();
// Textbased panels (i.e. ES|QL) should not save to library
const isTextBasedEmbeddable = isOfAggregateQueryType(query as AggregateQuery);
return Boolean(
canSave &&
isReferenceOrValueEmbeddable(embeddable) &&
!embeddable.inputIsRefType(embeddable.getInput()) &&
!isTextBasedEmbeddable
);
};
export const linkLegacyEmbeddable = async (embeddable: CommonLegacyEmbeddable) => {
const root = embeddable.getRoot() as IContainer;
if (!isReferenceOrValueEmbeddable(embeddable) || !apiIsPresentationContainer(root)) {
throw new IncompatibleActionError();
}
// Link to library
const newInput = await embeddable.getInputAsRefType();
embeddable.updateInput(newInput);
// Replace panel in parent.
const panelToReplace = root.getInput().panels[embeddable.id];
if (!panelToReplace) {
throw new PanelNotFoundError();
}
await root.replacePanel(panelToReplace.explicitInput.id, {
panelType: embeddable.type,
initialState: { ...newInput },
});
};

View file

@ -1,141 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ErrorEmbeddable, IContainer, PanelState, SavedObjectEmbeddableInput } from '../..';
import { embeddablePluginMock } from '../../../mocks';
import { createHelloWorldContainerAndEmbeddable } from '../../../tests/helpers';
import { ReferenceOrValueEmbeddable } from '../../reference_or_value_embeddable';
import {
ContactCardEmbeddable,
ContactCardEmbeddableFactory,
ContactCardEmbeddableInput,
} from '../../test_samples';
import { ViewMode } from '../../types';
import { CommonLegacyEmbeddable } from './legacy_embeddable_to_api';
import { canLinkLegacyEmbeddable } from './link_legacy_embeddable';
import { canUnlinkLegacyEmbeddable, unlinkLegacyEmbeddable } from './unlink_legacy_embeddable';
let container: IContainer;
let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable;
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
beforeEach(async () => {
const result = await createHelloWorldContainerAndEmbeddable();
container = result.container;
embeddable = embeddablePluginMock.mockRefOrValEmbeddable<
ContactCardEmbeddable,
ContactCardEmbeddableInput
>(result.embeddable, {
mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: result.embeddable.id },
mockedByValueInput: { firstName: 'Kibanana', id: result.embeddable.id },
});
embeddable.updateInput({ viewMode: ViewMode.EDIT });
});
test('Can unlink returns false when given an Error Embeddable', async () => {
const errorEmbeddable = new ErrorEmbeddable(
'Wow what an awful error',
{ id: ' 404' },
embeddable.getRoot() as IContainer
);
expect(
await canUnlinkLegacyEmbeddable(errorEmbeddable as unknown as CommonLegacyEmbeddable)
).toBe(false);
});
test('Can unlink returns true when embeddable on dashboard has reference type input', async () => {
embeddable.updateInput(await embeddable.getInputAsRefType());
expect(await canUnlinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(
true
);
});
test('Can unlink returns false when embeddable input is by value', async () => {
embeddable.updateInput(await embeddable.getInputAsValueType());
expect(await canUnlinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(
false
);
});
test('Can unlink returns false when view mode is set to view', async () => {
embeddable.updateInput(await embeddable.getInputAsRefType());
embeddable.updateInput({ viewMode: ViewMode.VIEW });
expect(await canUnlinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(
false
);
});
test('Can unlink returns false when embeddable is not in a dashboard container', async () => {
let orphanContactCard = await embeddableFactory.create({
id: 'orphanContact',
firstName: 'Orphan',
});
orphanContactCard = embeddablePluginMock.mockRefOrValEmbeddable<
ContactCardEmbeddable,
ContactCardEmbeddableInput
>(orphanContactCard, {
mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id },
mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id },
});
expect(
await canLinkLegacyEmbeddable(orphanContactCard as unknown as CommonLegacyEmbeddable)
).toBe(false);
expect(await canUnlinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable)).toBe(
false
);
});
test('Unlink replaces embeddableId and retains panel count', async () => {
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelCount = Object.keys(dashboard.getInput().panels).length;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
await unlinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable);
expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount);
const newPanelId = Object.keys(container.getInput().panels).find(
(key) => !originalPanelKeySet.has(key)
);
expect(newPanelId).toBeDefined();
const newPanel = container.getInput().panels[newPanelId!];
expect(newPanel.type).toEqual(embeddable.type);
});
test('Unlink unwraps all attributes from savedObject', async () => {
const complicatedAttributes = {
attribute1: 'The best attribute',
attribute2: 22,
attribute3: ['array', 'of', 'strings'],
attribute4: { nestedattribute: 'hello from the nest' },
};
embeddable = embeddablePluginMock.mockRefOrValEmbeddable<
ContactCardEmbeddable,
{ attributes: unknown; id: string },
SavedObjectEmbeddableInput
>(embeddable, {
mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id },
mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id },
});
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
await unlinkLegacyEmbeddable(embeddable as unknown as CommonLegacyEmbeddable);
const newPanelId = Object.keys(container.getInput().panels).find(
(key) => !originalPanelKeySet.has(key)
);
expect(newPanelId).toBeDefined();
const newPanel = container.getInput().panels[newPanelId!] as PanelState & {
explicitInput: { attributes: unknown };
};
expect(newPanel.type).toEqual(embeddable.type);
expect((newPanel.explicitInput as { attributes: unknown }).attributes).toEqual(
complicatedAttributes
);
});

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { IContainer, PanelState } from '../../containers';
import { PanelNotFoundError } from '../../errors';
import { isReferenceOrValueEmbeddable } from '../../reference_or_value_embeddable';
import { ViewMode } from '../../types';
import { isErrorEmbeddable } from '../is_error_embeddable';
import { EmbeddableInput } from '../i_embeddable';
import { CommonLegacyEmbeddable } from './legacy_embeddable_to_api';
export const canUnlinkLegacyEmbeddable = async (embeddable: CommonLegacyEmbeddable) => {
return Boolean(
isReferenceOrValueEmbeddable(embeddable) &&
!isErrorEmbeddable(embeddable) &&
embeddable.getInput()?.viewMode !== ViewMode.VIEW &&
embeddable.getRoot() &&
embeddable.getRoot().isContainer &&
embeddable.inputIsRefType(embeddable.getInput())
);
};
export const unlinkLegacyEmbeddable = async (embeddable: CommonLegacyEmbeddable) => {
const root = embeddable.getRoot() as IContainer;
if (!isReferenceOrValueEmbeddable(embeddable) || !apiIsPresentationContainer(root)) {
throw new IncompatibleActionError();
}
// unlink and update input.
const newInput = await embeddable.getInputAsValueType();
embeddable.updateInput(newInput);
// replace panel in parent.
const panelToReplace = root.getInput().panels[embeddable.id] as PanelState<EmbeddableInput>;
if (!panelToReplace) {
throw new PanelNotFoundError();
}
await root.replacePanel(panelToReplace.explicitInput.id, {
panelType: embeddable.type,
initialState: { ...newInput, title: embeddable.getTitle() },
});
};

View file

@ -1,163 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
/* eslint-disable max-classes-per-file */
import { skip, take } from 'rxjs';
import { Embeddable } from './embeddable';
import { EmbeddableOutput, EmbeddableInput } from './i_embeddable';
import { ViewMode } from '../types';
import { ContactCardEmbeddable } from '../test_samples/embeddables/contact_card/contact_card_embeddable';
import {
MockFilter,
FilterableEmbeddable,
} from '../test_samples/embeddables/filterable_embeddable';
class TestClass {
constructor() {}
}
interface Output extends EmbeddableOutput {
testClass: TestClass;
inputUpdatedTimes: number;
}
class OutputTestEmbeddable extends Embeddable<EmbeddableInput, Output> {
public readonly type = 'test';
constructor() {
super(
{ id: 'test', viewMode: ViewMode.VIEW },
{ testClass: new TestClass(), inputUpdatedTimes: 0 }
);
this.getInput$().subscribe(() => {
this.updateOutput({ inputUpdatedTimes: this.getOutput().inputUpdatedTimes + 1 });
});
}
reload() {}
}
class PhaseTestEmbeddable extends Embeddable<EmbeddableInput, EmbeddableOutput> {
public readonly type = 'phaseTest';
constructor() {
super({ id: 'phaseTest', viewMode: ViewMode.VIEW }, {});
}
public reportsEmbeddableLoad(): boolean {
return true;
}
reload() {}
}
test('Embeddable calls input subscribers when changed', (done) => {
const hello = new ContactCardEmbeddable(
{ id: '123', firstName: 'Brienne', lastName: 'Tarth' },
{ execAction: (() => null) as any }
);
const subscription = hello
.getInput$()
.pipe(skip(1))
.subscribe((input) => {
expect(input.nameTitle).toEqual('Sir');
done();
subscription.unsubscribe();
});
hello.updateInput({ nameTitle: 'Sir' });
});
test('Embeddable reload is called if lastReloadRequest input time changes', async () => {
const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 0 });
hello.reload = jest.fn();
hello.updateInput({ lastReloadRequestTime: 1 });
expect(hello.reload).toBeCalledTimes(1);
});
test('Embeddable reload is called if lastReloadRequest input time changed and new input is used', async () => {
const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 0 });
const aFilter = {} as unknown as MockFilter;
hello.reload = jest.fn(() => {
// when reload is called embeddable already has new input
expect(hello.getInput().filters).toEqual([aFilter]);
});
hello.updateInput({ lastReloadRequestTime: 1, filters: [aFilter] });
expect(hello.reload).toBeCalledTimes(1);
});
test('Embeddable reload is not called if lastReloadRequest input time does not change', async () => {
const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 1 });
hello.reload = jest.fn();
hello.updateInput({ lastReloadRequestTime: 1 });
expect(hello.reload).toBeCalledTimes(0);
});
test('updating output state retains instance information', async () => {
const outputTest = new OutputTestEmbeddable();
expect(outputTest.getOutput().testClass).toBeInstanceOf(TestClass);
expect(outputTest.getOutput().inputUpdatedTimes).toBe(1);
outputTest.updateInput({ viewMode: ViewMode.EDIT });
expect(outputTest.getOutput().inputUpdatedTimes).toBe(2);
expect(outputTest.getOutput().testClass).toBeInstanceOf(TestClass);
});
test('fires phase events when output changes', async () => {
const phaseEventTest = new PhaseTestEmbeddable();
let phaseEventCount = 0;
phaseEventTest.phase$.subscribe((event) => {
if (event) {
phaseEventCount++;
}
});
expect(phaseEventCount).toBe(1); // loading is true by default which fires an event.
phaseEventTest.updateOutput({ loading: false });
expect(phaseEventCount).toBe(2);
phaseEventTest.updateOutput({ rendered: true });
expect(phaseEventCount).toBe(3);
});
test('updated$ called after reload and batches input/output changes', async () => {
const hello = new ContactCardEmbeddable(
{ id: '123', firstName: 'Brienne', lastName: 'Tarth' },
{ execAction: (() => null) as any }
);
const reloadSpy = jest.spyOn(hello, 'reload');
const input$ = hello.getInput$().pipe(skip(1));
let inputEmittedTimes = 0;
input$.subscribe(() => {
inputEmittedTimes++;
});
const updated$ = hello.getUpdated$();
let updatedEmittedTimes = 0;
updated$.subscribe(() => {
updatedEmittedTimes++;
});
const updatedPromise = updated$.pipe(take(1)).toPromise();
hello.updateInput({ nameTitle: 'Sir', lastReloadRequestTime: Date.now() });
expect(reloadSpy).toHaveBeenCalledTimes(1);
expect(inputEmittedTimes).toBe(1);
expect(updatedEmittedTimes).toBe(0);
await updatedPromise;
expect(updatedEmittedTimes).toBe(1);
});

View file

@ -1,91 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { EuiCard, EuiFlexItem, EuiFlexGroup, EuiFormRow } from '@elastic/eui';
import { Subscription } from 'rxjs';
import { EuiButton } from '@elastic/eui';
import * as Rx from 'rxjs';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ContactCardEmbeddable, CONTACT_USER_TRIGGER } from './contact_card_embeddable';
interface Props {
embeddable: ContactCardEmbeddable;
execTrigger: UiActionsStart['executeTriggerActions'];
}
interface State {
fullName: string;
firstName: string;
}
export class ContactCardEmbeddableComponent extends React.Component<Props, State> {
private subscription?: Subscription;
private mounted: boolean = false;
constructor(props: Props) {
super(props);
this.state = {
fullName: this.props.embeddable.getOutput().fullName,
firstName: this.props.embeddable.getInput().firstName,
};
}
componentDidMount() {
this.mounted = true;
this.subscription = Rx.merge(
this.props.embeddable.getOutput$(),
this.props.embeddable.getInput$()
).subscribe(() => {
if (this.mounted) {
this.setState({
fullName: this.props.embeddable.getOutput().fullName,
firstName: this.props.embeddable.getInput().firstName,
});
}
});
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
}
this.mounted = false;
}
emitContactTrigger = () => {
this.props.execTrigger(CONTACT_USER_TRIGGER, {
embeddable: this.props.embeddable,
});
};
getCardFooterContent = () => (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiFormRow label="">
<EuiButton
onClick={this.emitContactTrigger}
>{`Contact ${this.state.firstName}`}</EuiButton>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
);
render() {
return (
<EuiCard
textAlign="left"
title={this.state.fullName}
footer={this.getCardFooterContent()}
description=""
/>
);
}
}

View file

@ -1,108 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import ReactDom from 'react-dom';
import { Subscription } from 'rxjs';
import type { ErrorLike } from '@kbn/expressions-plugin/common';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { Container } from '../../../containers';
import { EmbeddableOutput, Embeddable, EmbeddableInput } from '../../../embeddables';
import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory';
import { ContactCardEmbeddableComponent } from './contact_card';
export interface ContactCardEmbeddableInput extends EmbeddableInput {
firstName: string;
lastName?: string;
nameTitle?: string;
}
export interface ContactCardEmbeddableOutput extends EmbeddableOutput {
fullName: string;
originalLastName?: string;
}
export interface ContactCardEmbeddableOptions {
execAction: UiActionsStart['executeTriggerActions'];
outputOverrides?: Partial<ContactCardEmbeddableOutput>;
}
function getFullName(input: ContactCardEmbeddableInput) {
const { nameTitle, firstName, lastName } = input;
const nameParts = [nameTitle, firstName, lastName].filter((name) => name !== undefined);
return nameParts.join(' ');
}
export class ContactCardEmbeddable extends Embeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput
> {
private subscription: Subscription;
private node?: Element;
public readonly type: string = CONTACT_CARD_EMBEDDABLE;
constructor(
initialInput: ContactCardEmbeddableInput,
protected readonly options: ContactCardEmbeddableOptions,
parent?: Container
) {
super(
initialInput,
{
fullName: getFullName(initialInput),
originalLastName: initialInput.lastName,
defaultTitle: `Hello ${getFullName(initialInput)}`,
...options.outputOverrides,
},
parent
);
this.subscription = this.getInput$().subscribe(() => {
const fullName = getFullName(this.input);
this.updateOutput({
fullName,
defaultTitle: `Hello ${fullName}`,
});
});
}
public render(node: HTMLElement) {
this.node = node;
ReactDom.render(
<ContactCardEmbeddableComponent embeddable={this} execTrigger={this.options.execAction} />,
node
);
}
public catchError?(error: ErrorLike, node: HTMLElement) {
ReactDom.render(<div data-test-subj="error">{error.message}</div>, node);
return () => ReactDom.unmountComponentAtNode(node);
}
public destroy() {
super.destroy();
this.subscription.unsubscribe();
if (this.node) {
ReactDom.unmountComponentAtNode(this.node);
}
}
public reload() {}
public triggerError(error: ErrorLike, fatal = false) {
if (fatal) {
this.onFatalError(error);
} else {
this.updateOutput({ error });
}
}
}
export const CONTACT_USER_TRIGGER = 'CONTACT_USER_TRIGGER';

View file

@ -1,85 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { EmbeddableFactoryDefinition } from '../../../embeddables';
import { Container } from '../../../containers';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
import { ContactCardInitializer } from './contact_card_initializer';
export const CONTACT_CARD_EMBEDDABLE = 'CONTACT_CARD_EMBEDDABLE';
export class ContactCardEmbeddableFactory
implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput>
{
public readonly type = CONTACT_CARD_EMBEDDABLE;
savedObjectMetaData = {
name: 'Contact card',
type: CONTACT_CARD_EMBEDDABLE,
getIconForSavedObject: () => 'document',
};
constructor(
protected readonly execTrigger: UiActionsStart['executeTriggerActions'],
private readonly core: CoreStart
) {}
public async isEditable() {
return true;
}
public getDisplayName() {
return i18n.translate('embeddableApi.samples.contactCard.displayName', {
defaultMessage: 'contact card',
});
}
public getDefaultInput() {
return {};
}
public getExplicitInput = (): Promise<Partial<ContactCardEmbeddableInput>> => {
return new Promise((resolve) => {
const modalSession = this.core.overlays.openModal(
toMountPoint(
<ContactCardInitializer
onCancel={() => {
modalSession.close();
// @ts-expect-error
resolve(undefined);
}}
onCreate={(input: { firstName: string; lastName?: string }) => {
modalSession.close();
resolve(input);
}}
/>,
this.core
),
{
'data-test-subj': 'createContactCardEmbeddable',
}
);
});
};
public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => {
return new ContactCardEmbeddable(
initialInput,
{
execAction: this.execTrigger,
},
parent
);
};
}

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { ContactCardEmbeddableComponent } from './contact_card';
import { ContactCardEmbeddable } from './contact_card_embeddable';
export class ContactCardEmbeddableReact extends ContactCardEmbeddable {
public render() {
return (
<ContactCardEmbeddableComponent embeddable={this} execTrigger={this.options.execAction} />
);
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Container } from '../../../containers';
import { ContactCardEmbeddableInput } from './contact_card_embeddable';
import { ContactCardEmbeddableFactory } from './contact_card_embeddable_factory';
import { ContactCardEmbeddableReact } from './contact_card_embeddable_react';
export const CONTACT_CARD_EMBEDDABLE_REACT = 'CONTACT_CARD_EMBEDDABLE_REACT';
export class ContactCardEmbeddableReactFactory extends ContactCardEmbeddableFactory {
public readonly type = CONTACT_CARD_EMBEDDABLE_REACT as ContactCardEmbeddableFactory['type'];
public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => {
return new ContactCardEmbeddableReact(
initialInput,
{
execAction: this.execTrigger,
},
parent
);
};
}

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ContactCardEmbeddable } from './contact_card_embeddable';
export class ContactCardExportableEmbeddable extends ContactCardEmbeddable {
public getInspectorAdapters = () => {
return {
tables: {
allowCsvExport: true,
tables: {
layer1: {
type: 'datatable',
columns: [
{ id: 'firstName', name: 'First Name' },
{ id: 'originalLastName', name: 'Last Name' },
],
rows: [
{
firstName: this.getInput().firstName,
orignialLastName: this.getInput().lastName,
},
],
},
},
},
};
};
}
export const CONTACT_EXPORTABLE_USER_TRIGGER = 'CONTACT_EXPORTABLE_USER_TRIGGER';

View file

@ -1,81 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { EmbeddableFactoryDefinition } from '../../../embeddables';
import { Container } from '../../../containers';
import { ContactCardEmbeddableInput } from './contact_card_embeddable';
import { ContactCardExportableEmbeddable } from './contact_card_exportable_embeddable';
import { ContactCardInitializer } from './contact_card_initializer';
export const CONTACT_CARD_EXPORTABLE_EMBEDDABLE = 'CONTACT_CARD_EXPORTABLE_EMBEDDABLE';
export class ContactCardExportableEmbeddableFactory
implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput>
{
public readonly type = CONTACT_CARD_EXPORTABLE_EMBEDDABLE;
constructor(
private readonly execTrigger: UiActionsStart['executeTriggerActions'],
private readonly core: CoreStart
) {}
public async isEditable() {
return true;
}
public getDefaultInput() {
return {};
}
public getDisplayName() {
return i18n.translate('embeddableApi.samples.contactCard.displayName', {
defaultMessage: 'contact card',
});
}
public getExplicitInput = (): Promise<Partial<ContactCardEmbeddableInput>> => {
return new Promise((resolve) => {
const modalSession = this.core.overlays.openModal(
toMountPoint(
<ContactCardInitializer
onCancel={() => {
modalSession.close();
// @ts-expect-error
resolve(undefined);
}}
onCreate={(input: { firstName: string; lastName?: string }) => {
modalSession.close();
resolve(input);
}}
/>,
this.core
),
{
'data-test-subj': 'createContactCardEmbeddable',
}
);
});
};
public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => {
return new ContactCardExportableEmbeddable(
initialInput,
{
execAction: this.execTrigger,
},
parent
);
};
}

View file

@ -1,88 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
EuiForm,
EuiFormRow,
EuiFieldText,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiButton,
EuiModalFooter,
EuiButtonEmpty,
} from '@elastic/eui';
import React, { Component } from 'react';
export interface ContactCardInitializerProps {
onCreate: (name: { lastName?: string; firstName: string }) => void;
onCancel: () => void;
}
interface State {
firstName?: string;
lastName?: string;
}
export class ContactCardInitializer extends Component<ContactCardInitializerProps, State> {
constructor(props: ContactCardInitializerProps) {
super(props);
this.state = {};
}
render() {
return (
<div>
<EuiModalHeader>
<EuiModalHeaderTitle>Create a new greeting card</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiForm>
<EuiFormRow label="First name">
<EuiFieldText
name="popfirst"
value={this.state.firstName}
onChange={(e) => this.setState({ firstName: e.target.value })}
/>
</EuiFormRow>
<EuiFormRow label="Last name">
<EuiFieldText
name="popfirst"
value={this.state.lastName}
placeholder="optional"
onChange={(e) => this.setState({ lastName: e.target.value })}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={this.props.onCancel}>Cancel</EuiButtonEmpty>
<EuiButton
isDisabled={!this.state.firstName}
onClick={() => {
if (this.state.firstName) {
this.props.onCreate({
firstName: this.state.firstName,
...(this.state.lastName ? { lastName: this.state.lastName } : {}),
});
}
}}
fill
>
Create
</EuiButton>
</EuiModalFooter>
</div>
);
}
}

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { Container, EmbeddableFactoryDefinition } from '../../..';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
export const DESCRIPTIVE_CONTACT_CARD_EMBEDDABLE = 'DESCRIPTIVE_CONTACT_CARD_EMBEDDABLE';
export class DescriptiveContactCardEmbeddableFactory
implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput>
{
public readonly type = DESCRIPTIVE_CONTACT_CARD_EMBEDDABLE;
constructor(protected readonly execTrigger: UiActionsStart['executeTriggerActions']) {}
public async isEditable() {
return true;
}
public getDisplayName() {
return 'descriptive contact card';
}
public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => {
return new ContactCardEmbeddable(
initialInput,
{
execAction: this.execTrigger,
outputOverrides: {
defaultDescription: 'This is a family friend',
},
},
parent
);
};
}

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './contact_card';
export * from './contact_card_embeddable';
export * from './contact_card_embeddable_factory';
export * from './contact_card_exportable_embeddable';
export * from './contact_card_exportable_embeddable_factory';
export * from './contact_card_embeddable_react';
export * from './contact_card_embeddable_react_factory';
export * from './contact_card_initializer';
export * from './slow_contact_card_embeddable_factory';

View file

@ -1,46 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { Container, EmbeddableFactoryDefinition } from '../../..';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory';
interface SlowContactCardEmbeddableFactoryOptions {
execAction: UiActionsStart['executeTriggerActions'];
loadTickCount?: number;
}
export class SlowContactCardEmbeddableFactory
implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput>
{
private loadTickCount = 0;
public readonly type = CONTACT_CARD_EMBEDDABLE;
constructor(private readonly options: SlowContactCardEmbeddableFactoryOptions) {
if (options.loadTickCount) {
this.loadTickCount = options.loadTickCount;
}
}
public async isEditable() {
return true;
}
public getDisplayName() {
return 'slow to load contact card';
}
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

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Embeddable, EmbeddableInput, EmbeddableOutput } from '../..';
export const EMPTY_EMBEDDABLE = 'EMPTY_EMBEDDABLE';
export class EmptyEmbeddable extends Embeddable<EmbeddableInput, EmbeddableOutput> {
public readonly type = EMPTY_EMBEDDABLE;
constructor(initialInput: EmbeddableInput) {
super(initialInput, {});
}
public render() {}
public reload() {}
}

View file

@ -1,52 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Container, ContainerInput } from '../../containers';
import { EmbeddableStart } from '../../../plugin';
import { MockFilter } from './filterable_embeddable';
export const FILTERABLE_CONTAINER = 'FILTERABLE_CONTAINER';
export interface FilterableContainerInput extends ContainerInput {
filters: MockFilter[];
}
/**
* interfaces are not allowed to specify a sub-set of the required types until
* https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type
* here instead
*/
type InheritedChildrenInput = {
filters: MockFilter[];
id?: string;
};
export class FilterableContainer extends Container<
InheritedChildrenInput,
FilterableContainerInput
> {
public readonly type = FILTERABLE_CONTAINER;
constructor(
initialInput: FilterableContainerInput,
getFactory: EmbeddableStart['getEmbeddableFactory'],
parent?: Container
) {
super(initialInput, { embeddableLoaded: {} }, getFactory, parent);
}
public getInheritedInput() {
return {
filters: this.input.filters,
viewMode: this.input.viewMode,
};
}
public render() {}
}

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import { Container, EmbeddableFactoryDefinition } from '../..';
import {
FilterableContainer,
FilterableContainerInput,
FILTERABLE_CONTAINER,
} from './filterable_container';
import { EmbeddableStart } from '../../../plugin';
export class FilterableContainerFactory
implements EmbeddableFactoryDefinition<FilterableContainerInput>
{
public readonly type = FILTERABLE_CONTAINER;
constructor(
private readonly getFactory: () => Promise<EmbeddableStart['getEmbeddableFactory']>
) {}
public getDisplayName() {
return i18n.translate('embeddableApi.samples.filterableContainer.displayName', {
defaultMessage: 'filterable dashboard',
});
}
public async isEditable() {
return true;
}
public create = async (initialInput: FilterableContainerInput, parent?: Container) => {
const getEmbeddableFactory = await this.getFactory();
return new FilterableContainer(initialInput, getEmbeddableFactory, parent);
};
}

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { IContainer } from '../../containers';
import { EmbeddableOutput, EmbeddableInput, Embeddable } from '../../embeddables';
/** @internal */
export interface MockFilter {
$state?: {};
meta: {};
query?: {};
}
export const FILTERABLE_EMBEDDABLE = 'FILTERABLE_EMBEDDABLE';
export interface FilterableEmbeddableInput extends EmbeddableInput {
filters: MockFilter[];
}
export class FilterableEmbeddable extends Embeddable<FilterableEmbeddableInput, EmbeddableOutput> {
public readonly type = FILTERABLE_EMBEDDABLE;
constructor(initialInput: FilterableEmbeddableInput, parent?: IContainer) {
super(initialInput, {}, parent);
}
public getInspectorAdapters(): Record<string, string> {
const inspectorAdapters: Record<string, string> = {
filters: `My filters are ${JSON.stringify(this.input.filters)}`,
};
return inspectorAdapters;
}
public render() {}
public reload() {}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import {
FilterableEmbeddable,
FilterableEmbeddableInput,
FILTERABLE_EMBEDDABLE,
} from './filterable_embeddable';
import { EmbeddableFactoryDefinition } from '../../embeddables';
import { IContainer } from '../../containers';
export class FilterableEmbeddableFactory
implements EmbeddableFactoryDefinition<FilterableEmbeddableInput>
{
public readonly type = FILTERABLE_EMBEDDABLE;
public async isEditable() {
return true;
}
public getDisplayName() {
return i18n.translate('embeddableApi.samples.filterableEmbeddable.displayName', {
defaultMessage: 'filterable',
});
}
public async create(initialInput: FilterableEmbeddableInput, parent?: IContainer) {
return new FilterableEmbeddable(initialInput, parent);
}
}

View file

@ -1,72 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n-react';
import { Container, ViewMode, ContainerInput } from '../..';
import { HelloWorldContainerComponent } from './hello_world_container_component';
import { EmbeddableStart } from '../../../plugin';
import { EmbeddableContainerSettings } from '../../containers/i_container';
export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER';
/**
* interfaces are not allowed to specify a sub-set of the required types until
* https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type
* here instead
*/
type InheritedInput = {
id: string;
viewMode: ViewMode;
lastName: string;
};
interface HelloWorldContainerInput extends ContainerInput {
lastName?: string;
}
interface HelloWorldContainerOptions {
getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'];
}
export class HelloWorldContainer extends Container<InheritedInput, HelloWorldContainerInput> {
public readonly type = HELLO_WORLD_CONTAINER;
constructor(
input: ContainerInput<{ firstName: string; lastName: string }>,
options: HelloWorldContainerOptions,
initializeSettings?: EmbeddableContainerSettings
) {
super(
input,
{ embeddableLoaded: {} },
options.getEmbeddableFactory || (() => undefined),
undefined,
initializeSettings
);
}
public getInheritedInput(id: string) {
return {
id,
viewMode: this.input.viewMode || ViewMode.EDIT,
lastName: this.input.lastName || 'foo',
};
}
public render(node: HTMLElement) {
ReactDOM.render(
<I18nProvider>
<HelloWorldContainerComponent container={this} />
</I18nProvider>,
node
);
}
}

View file

@ -1,100 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { Component, RefObject } from 'react';
import { Subscription } from 'rxjs';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { IContainer, PanelState } from '../..';
import { EmbeddablePanel } from '../../../embeddable_panel';
interface Props {
container: IContainer;
}
interface State {
panels: { [key: string]: PanelState };
loaded: { [key: string]: boolean };
}
export class HelloWorldContainerComponent extends Component<Props, State> {
private roots: { [key: string]: RefObject<HTMLDivElement> } = {};
private mounted: boolean = false;
private inputSubscription?: Subscription;
private outputSubscription?: Subscription;
constructor(props: Props) {
super(props);
Object.values(this.props.container.getInput().panels).forEach((panelState) => {
this.roots[panelState.explicitInput.id] = React.createRef();
});
this.state = {
loaded: this.props.container.getOutput().embeddableLoaded,
panels: this.props.container.getInput().panels,
};
}
public async componentDidMount() {
this.mounted = true;
this.inputSubscription = this.props.container.getInput$().subscribe(() => {
if (this.mounted) {
this.setState({ panels: this.props.container.getInput().panels });
}
});
this.outputSubscription = this.props.container.getOutput$().subscribe(() => {
if (this.mounted) {
this.setState({ loaded: this.props.container.getOutput().embeddableLoaded });
}
});
}
public componentWillUnmount() {
this.mounted = false;
this.props.container.destroy();
if (this.inputSubscription) {
this.inputSubscription.unsubscribe();
}
if (this.outputSubscription) {
this.outputSubscription.unsubscribe();
}
}
public render() {
return (
<div>
<h2>HELLO WORLD! These are my precious embeddable children:</h2>
<EuiSpacer size="l" />
<EuiFlexGroup>{this.renderList()}</EuiFlexGroup>
</div>
);
}
private renderList() {
const list = Object.values(this.state.panels).map((panelState) => {
const item = (
<EuiFlexItem key={panelState.explicitInput.id}>
<EmbeddablePanel
embeddable={() =>
this.props.container.untilEmbeddableLoaded(panelState.explicitInput.id)
}
/>
</EuiFlexItem>
);
return item;
});
return list;
}
}

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './contact_card';
export * from './empty_embeddable';
export * from './filterable_container';
export * from './filterable_container_factory';
export * from './filterable_embeddable';
export * from './filterable_embeddable_factory';
export * from './hello_world_container';
export * from './hello_world_container_component';
export * from './time_range_container';
export * from './time_range_embeddable_factory';
export * from './time_range_embeddable';

View file

@ -1,54 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { TimeRange } from '@kbn/es-query';
import { ContainerInput, Container, ContainerOutput, EmbeddableStart } from '../../..';
/**
* interfaces are not allowed to specify a sub-set of the required types until
* https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type
* here instead
*/
type InheritedChildrenInput = {
timeRange: TimeRange;
id?: string;
};
interface ContainerTimeRangeInput extends ContainerInput<InheritedChildrenInput> {
timeRange: TimeRange;
}
const TIME_RANGE_CONTAINER = 'TIME_RANGE_CONTAINER';
export class TimeRangeContainer extends Container<
InheritedChildrenInput,
ContainerTimeRangeInput,
ContainerOutput
> {
public readonly type = TIME_RANGE_CONTAINER;
constructor(
initialInput: ContainerTimeRangeInput,
getFactory: EmbeddableStart['getEmbeddableFactory'],
parent?: Container
) {
super(initialInput, { embeddableLoaded: {} }, getFactory, parent);
}
public getAllDataViews() {
return [];
}
public getInheritedInput() {
return { timeRange: this.input.timeRange };
}
public render() {}
public reload() {}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { TimeRange } from '@kbn/es-query';
import { EmbeddableOutput, Embeddable, EmbeddableInput, IContainer } from '../../..';
export interface EmbeddableTimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
export const TIME_RANGE_EMBEDDABLE = 'TIME_RANGE_EMBEDDABLE';
export class TimeRangeEmbeddable extends Embeddable<EmbeddableTimeRangeInput, EmbeddableOutput> {
public readonly type = TIME_RANGE_EMBEDDABLE;
constructor(initialInput: EmbeddableTimeRangeInput, parent?: IContainer) {
const { title: defaultTitle, description: defaultDescription } = initialInput;
super(
initialInput,
{
defaultTitle,
defaultDescription,
},
parent
);
}
public render() {}
public reload() {}
}

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { IContainer, EmbeddableFactoryDefinition } from '../../..';
import {
TIME_RANGE_EMBEDDABLE,
TimeRangeEmbeddable,
EmbeddableTimeRangeInput,
} from './time_range_embeddable';
export class TimeRangeEmbeddableFactory
implements EmbeddableFactoryDefinition<EmbeddableTimeRangeInput>
{
public readonly type = TIME_RANGE_EMBEDDABLE;
public async isEditable() {
return true;
}
public async create(initialInput: EmbeddableTimeRangeInput, parent?: IContainer) {
return new TimeRangeEmbeddable(initialInput, parent);
}
public getDisplayName() {
return 'time range';
}
}

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './embeddables';

View file

@ -1,817 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { coreMock } from '@kbn/core/public/mocks';
import * as Rx from 'rxjs';
import { skip } from 'rxjs';
import { EmbeddableOutput, isErrorEmbeddable, SavedObjectEmbeddableInput, ViewMode } from '../lib';
import { ERROR_EMBEDDABLE_TYPE } from '../lib/embeddables/error_embeddable';
import {
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
} from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable';
import { CONTACT_CARD_EMBEDDABLE } from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { SlowContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory';
import {
FilterableContainer,
FilterableContainerInput,
} from '../lib/test_samples/embeddables/filterable_container';
import {
FilterableEmbeddable,
FilterableEmbeddableInput,
FILTERABLE_EMBEDDABLE,
MockFilter,
} from '../lib/test_samples/embeddables/filterable_embeddable';
import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world_container';
import { HelloWorldEmbeddableFactoryDefinition, HELLO_WORLD_EMBEDDABLE } from './fixtures';
import { createHelloWorldContainerAndEmbeddable, of } from './helpers';
import { testPlugin } from './test_plugin';
describe('container initialization', () => {
const panels = {
'123': {
explicitInput: { id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
},
'456': {
explicitInput: { id: '456' },
type: CONTACT_CARD_EMBEDDABLE,
},
'789': {
explicitInput: { id: '789' },
type: CONTACT_CARD_EMBEDDABLE,
},
};
const expectEmbeddableLoaded = (container: HelloWorldContainer, id: string) => {
expect(container.getOutput().embeddableLoaded['123']).toBe(true);
const embeddable = container.getChild<ContactCardEmbeddable>('123');
expect(embeddable).toBeDefined();
expect(embeddable.id).toBe('123');
};
it('initializes embeddables', async () => {
const { container } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels,
});
expectEmbeddableLoaded(container, '123');
expectEmbeddableLoaded(container, '456');
expectEmbeddableLoaded(container, '789');
});
it('initializes embeddables once and only once with multiple input updates', async () => {
const { container, contactCardCreateSpy } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels,
});
container.updateInput({ lastReloadRequestTime: 1 });
container.updateInput({ lastReloadRequestTime: 2 });
expect(contactCardCreateSpy).toHaveBeenCalledTimes(4);
});
it('initializes embeddables in order', async () => {
const childIdInitializeOrder = ['456', '123', '789'];
const { contactCardCreateSpy } = await createHelloWorldContainerAndEmbeddable(
{
id: 'hello',
panels,
},
{},
{ childIdInitializeOrder }
);
await new Promise((r) => setTimeout(r, 1));
for (const [index, orderedId] of childIdInitializeOrder.entries()) {
expect(contactCardCreateSpy).toHaveBeenNthCalledWith(
index + 1,
expect.objectContaining({ id: orderedId }),
expect.anything() // parent passed into create method
);
}
});
it('initializes embeddables in order with partial order arg', async () => {
const childIdInitializeOrder = ['789', 'idontexist'];
const { contactCardCreateSpy } = await createHelloWorldContainerAndEmbeddable(
{
id: 'hello',
panels,
},
{},
{ childIdInitializeOrder }
);
const expectedInitializeOrder = ['789', '123', '456'];
await new Promise((r) => setTimeout(r, 1));
for (const [index, orderedId] of expectedInitializeOrder.entries()) {
expect(contactCardCreateSpy).toHaveBeenNthCalledWith(
index + 1,
expect.objectContaining({ id: orderedId }),
expect.anything() // parent passed into create method
);
}
});
it('initializes embeddables in order, awaiting each', async () => {
const childIdInitializeOrder = ['456', '123', '789'];
const { container, contactCardCreateSpy } = await createHelloWorldContainerAndEmbeddable(
{
id: 'hello',
panels,
},
{},
{ childIdInitializeOrder, initializeSequentially: true }
);
const untilEmbeddableLoadedMock = jest.spyOn(container, 'untilEmbeddableLoaded');
await new Promise((r) => setTimeout(r, 10));
for (const [index, orderedId] of childIdInitializeOrder.entries()) {
await container.untilEmbeddableLoaded(orderedId);
expect(contactCardCreateSpy).toHaveBeenNthCalledWith(
index + 1,
expect.objectContaining({ id: orderedId }),
expect.anything() // parent passed into create method
);
expect(untilEmbeddableLoadedMock).toHaveBeenCalledWith(orderedId);
}
});
});
test('Container.addNewEmbeddable', async () => {
const { container, embeddable } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {} },
{
firstName: 'Susy',
}
);
expect(embeddable).toBeDefined();
if (!isErrorEmbeddable(embeddable)) {
expect(embeddable.getInput().firstName).toBe('Susy');
} else {
expect(false).toBe(true);
}
const embeddableInContainer = container.getChild<ContactCardEmbeddable>(embeddable.id);
expect(embeddableInContainer).toBeDefined();
expect(embeddableInContainer.id).toBe(embeddable.id);
});
test('Container.removeEmbeddable removes and cleans up', async () => {
const { start } = await createHelloWorldContainerAndEmbeddable();
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { id: '123', firstName: 'Sam', lastName: 'Tarley' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
const embeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
firstName: 'Susy',
lastName: 'Q',
});
if (isErrorEmbeddable(embeddable)) {
expect(false).toBe(true);
return;
}
embeddable.updateInput({ lastName: 'Z' });
container
.getOutput$()
.pipe(skip(1))
.subscribe(() => {
const noFind = container.getChild<ContactCardEmbeddable>(embeddable.id);
expect(noFind).toBeUndefined();
expect(container.getInput().panels[embeddable.id]).toBeUndefined();
if (isErrorEmbeddable(embeddable)) {
expect(false).toBe(true);
}
expect(() => embeddable.updateInput({ nameTitle: 'Sir' })).toThrowError();
expect(container.getOutput().embeddableLoaded[embeddable.id]).toBeUndefined();
});
container.removeEmbeddable(embeddable.id);
});
test('Container.input$ is notified when child embeddable input is updated', async () => {
const { container, embeddable } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {} },
{
firstName: 'Susy',
lastName: 'Q',
}
);
expect(isErrorEmbeddable(embeddable)).toBe(false);
const changes = jest.fn();
expect(changes).toHaveBeenCalledTimes(0);
const subscription = container.getInput$().subscribe(changes);
expect(changes).toHaveBeenCalledTimes(1);
embeddable.updateInput({ lastName: 'Z' });
expect(changes).toHaveBeenCalledTimes(2);
expect(embeddable.getInput().lastName === 'Z');
embeddable.updateInput({ lastName: embeddable.getOutput().originalLastName });
expect(embeddable.getInput().lastName === 'Q');
expect(changes).toBeCalledTimes(3);
subscription.unsubscribe();
embeddable.updateInput({ nameTitle: 'Dr.' });
expect(changes).toBeCalledTimes(3);
});
test('Container.input$', async () => {
const { container, embeddable } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {} },
{
firstName: 'Susy',
id: 'Susy',
}
);
expect(isErrorEmbeddable(embeddable)).toBe(false);
const changes = jest.fn();
const input = container.getInput();
expect(input.panels[embeddable.id].explicitInput).toEqual({ firstName: 'Susy', id: 'Susy' });
const subscription = container.getInput$().subscribe(changes);
embeddable.updateInput({ nameTitle: 'Dr.' });
expect(container.getInput().panels[embeddable.id].explicitInput).toEqual({
nameTitle: 'Dr.',
firstName: 'Susy',
id: 'Susy',
});
subscription.unsubscribe();
});
test('Container.getInput$ not triggered if state is the same', async () => {
const { container, embeddable } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {} },
{
firstName: 'Susy',
id: 'Susy',
}
);
expect(isErrorEmbeddable(embeddable)).toBe(false);
const changes = jest.fn();
const input = container.getInput();
expect(input.panels[embeddable.id].explicitInput).toEqual({
id: 'Susy',
firstName: 'Susy',
});
const subscription = container.getInput$().subscribe(changes);
embeddable.updateInput({ nameTitle: 'Dr.' });
expect(changes).toBeCalledTimes(2);
embeddable.updateInput({ nameTitle: 'Dr.' });
expect(changes).toBeCalledTimes(2);
subscription.unsubscribe();
});
test('Container view mode change propagates to children', async () => {
const { container, embeddable } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {}, viewMode: ViewMode.VIEW },
{
firstName: 'Susy',
id: 'Susy',
}
);
expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW);
container.updateInput({ viewMode: ViewMode.EDIT });
expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT);
});
test(`Container updates its state when a child's input is updated`, async () => {
const { container, embeddable, start } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {}, viewMode: ViewMode.VIEW },
{
id: '123',
firstName: 'Susy',
}
);
expect(isErrorEmbeddable(embeddable)).toBe(false);
const containerSubscription = Rx.merge(container.getInput$(), container.getOutput$()).subscribe(
() => {
const child = container.getChild<ContactCardEmbeddable>(embeddable.id);
if (
container.getOutput().embeddableLoaded[embeddable.id] &&
child.getInput().nameTitle === 'Dr.'
) {
containerSubscription.unsubscribe();
// Make sure a brand new container built off the output of container also creates an embeddable
// with "Dr.", not the default the embeddable was first added with. Makes sure changed input
// is preserved with the container.
const containerClone = new HelloWorldContainer(container.getInput(), {
getEmbeddableFactory: start.getEmbeddableFactory,
});
const cloneSubscription = Rx.merge(
containerClone.getOutput$(),
containerClone.getInput$()
).subscribe(() => {
const childClone = containerClone.getChild<ContactCardEmbeddable>(embeddable.id);
if (
containerClone.getOutput().embeddableLoaded[embeddable.id] &&
childClone.getInput().nameTitle === 'Dr.'
) {
cloneSubscription.unsubscribe();
}
});
}
}
);
embeddable.updateInput({ nameTitle: 'Dr.' });
});
test(`Derived container state passed to children`, async () => {
const { container, embeddable } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {}, viewMode: ViewMode.VIEW },
{
firstName: 'Susy',
}
);
let subscription = embeddable
.getInput$()
.pipe(skip(1))
.subscribe((changes: Partial<ContactCardEmbeddableInput>) => {
expect(changes.viewMode).toBe(ViewMode.EDIT);
});
container.updateInput({ viewMode: ViewMode.EDIT });
subscription.unsubscribe();
subscription = embeddable
.getInput$()
.pipe(skip(1))
.subscribe((changes: Partial<ContactCardEmbeddableInput>) => {
expect(changes.viewMode).toBe(ViewMode.VIEW);
});
container.updateInput({ viewMode: ViewMode.VIEW });
subscription.unsubscribe();
});
test(`Can subscribe to children embeddable updates`, async () => {
const { embeddable } = await createHelloWorldContainerAndEmbeddable(
{
id: 'hello container',
panels: {},
viewMode: ViewMode.VIEW,
},
{
firstName: 'Susy',
}
);
expect(isErrorEmbeddable(embeddable)).toBe(false);
const subscription = embeddable.getInput$().subscribe((input: ContactCardEmbeddableInput) => {
if (input.nameTitle === 'Dr.') {
subscription.unsubscribe();
}
});
embeddable.updateInput({ nameTitle: 'Dr.' });
});
test('Test nested reactions', async () => {
const { container, embeddable } = await createHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {}, viewMode: ViewMode.VIEW },
{
firstName: 'Susy',
}
);
expect(isErrorEmbeddable(embeddable)).toBe(false);
const containerSubscription = container.getInput$().subscribe((input: any) => {
const embeddableNameTitle = embeddable.getInput().nameTitle;
const viewMode = input.viewMode;
const nameTitleFromContainer = container.getInputForChild<ContactCardEmbeddableInput>(
embeddable.id
).nameTitle;
if (
embeddableNameTitle === 'Dr.' &&
nameTitleFromContainer === 'Dr.' &&
viewMode === ViewMode.EDIT
) {
containerSubscription.unsubscribe();
embeddableSubscription.unsubscribe();
}
});
const embeddableSubscription = embeddable.getInput$().subscribe(() => {
if (embeddable.getInput().nameTitle === 'Dr.') {
container.updateInput({ viewMode: ViewMode.EDIT });
}
});
embeddable.updateInput({ nameTitle: 'Dr.' });
});
test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const { start } = await createHelloWorldContainerAndEmbeddable();
const derivedFilter: MockFilter = {
$state: { store: 'appState' },
meta: { disabled: false, alias: 'name', negate: false },
query: { match: {} },
};
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [derivedFilter] },
start.getEmbeddableFactory
);
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
embeddable.updateInput({ filters: undefined });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([
derivedFilter,
]);
});
test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async () => {
const { container } = await createHelloWorldContainerAndEmbeddable({ id: 'hello', panels: {} });
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
const subscription = embeddable
.getInput$()
.pipe(skip(1))
.subscribe(() => {
if (embeddable.getInput().filters === undefined) {
subscription.unsubscribe();
}
});
embeddable.updateInput({ filters: undefined });
});
test('Panel removed from input state', async () => {
const { container } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels: {},
});
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
const filteredPanels = { ...container.getInput().panels };
delete filteredPanels[embeddable.id];
const newInput: Partial<FilterableContainerInput> = {
...container.getInput(),
panels: filteredPanels,
};
container.updateInput(newInput);
await new Promise((r) => setTimeout(r, 1));
expect(container.getChild(embeddable.id)).toBeUndefined();
expect(container.getOutput().embeddableLoaded[embeddable.id]).toBeUndefined();
});
test('Panel added to input state', async () => {
const { container, start } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels: {},
});
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
const embeddable2 = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
const container2 = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [] },
start.getEmbeddableFactory
);
container2.updateInput(container.getInput());
await new Promise((r) => setTimeout(r, 1));
expect(container.getChild(embeddable.id)).toBeDefined();
expect(container.getOutput().embeddableLoaded[embeddable.id]).toBe(true);
expect(container.getChild(embeddable2.id)).toBeDefined();
expect(container.getOutput().embeddableLoaded[embeddable2.id]).toBe(true);
});
test('Container changes made directly after adding a new embeddable are propagated', async () => {
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(
{
id: 'hello',
panels: {},
viewMode: ViewMode.EDIT,
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
const subscription = Rx.merge(container.getOutput$(), container.getInput$())
.pipe(skip(2))
.subscribe(() => {
expect(Object.keys(container.getOutput().embeddableLoaded).length).toBe(1);
if (Object.keys(container.getOutput().embeddableLoaded).length > 0) {
const embeddableId = Object.keys(container.getOutput().embeddableLoaded)[0];
if (container.getOutput().embeddableLoaded[embeddableId] === true) {
const embeddable = container.getChild(embeddableId);
if (embeddable.getInput().viewMode === ViewMode.VIEW) {
subscription.unsubscribe();
}
}
}
});
container.addNewEmbeddable<ContactCardEmbeddableInput>(CONTACT_CARD_EMBEDDABLE, {
firstName: 'A girl',
lastName: 'has no name',
});
container.updateInput({ viewMode: ViewMode.VIEW });
});
test('container stores ErrorEmbeddables when a factory for a child cannot be found (so the panel can be removed)', async () => {
const { container } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels: {
'123': {
type: 'IDontExist',
explicitInput: { id: '123' },
},
},
viewMode: ViewMode.EDIT,
});
container.getOutput$().subscribe(() => {
if (container.getOutput().embeddableLoaded['123']) {
const child = container.getChild('123');
expect(child.type).toBe(ERROR_EMBEDDABLE_TYPE);
}
});
});
test('container stores ErrorEmbeddables when a saved object cannot be found', async () => {
const { container } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels: {
'123': {
type: 'vis',
explicitInput: { id: '123', savedObjectId: '456' } as SavedObjectEmbeddableInput,
},
},
viewMode: ViewMode.EDIT,
});
container.getOutput$().subscribe(() => {
if (container.getOutput().embeddableLoaded['123']) {
const child = container.getChild('123');
expect(child.type).toBe(ERROR_EMBEDDABLE_TYPE);
}
});
});
test('ErrorEmbeddables get updated when parent does', async () => {
const { container } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels: {
'123': {
type: 'vis',
explicitInput: { id: '123', savedObjectId: '456' } as SavedObjectEmbeddableInput,
},
},
viewMode: ViewMode.EDIT,
});
container.getOutput$().subscribe(() => {
if (container.getOutput().embeddableLoaded['123']) {
const embeddable = container.getChild('123');
expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT);
container.updateInput({ viewMode: ViewMode.VIEW });
expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW);
}
});
});
test('untilEmbeddableLoaded() throws an error if there is no such child panel in the container', async () => {
const { container } = await createHelloWorldContainerAndEmbeddable({
id: 'hello',
panels: {},
});
expect(container.untilEmbeddableLoaded('idontexist')).rejects.toThrowError();
});
test('untilEmbeddableLoaded() throws an error if there is no such child panel in the container - 2', async () => {
const { doStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {},
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
const [, error] = await of(container.untilEmbeddableLoaded('123'));
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toMatchInlineSnapshot(`"Panel not found"`);
});
test('untilEmbeddableLoaded() resolves if child is loaded in the container', async () => {
const { setup, doStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const factory = new HelloWorldEmbeddableFactoryDefinition();
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: { id: '123' },
},
},
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
const child = await container.untilEmbeddableLoaded('123');
expect(child).toBeDefined();
expect(child.type).toBe(HELLO_WORLD_EMBEDDABLE);
});
test('untilEmbeddableLoaded resolves with undefined if child is subsequently removed', async () => {
const { doStart, setup, uiActions } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { id: '123', firstName: 'Sam', lastName: 'Tarley' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
container.untilEmbeddableLoaded('123').then((embed) => {
expect(embed).toBeUndefined();
});
container.updateInput({ panels: {} });
});
test('adding a panel then subsequently removing it before its loaded removes the panel', (done) => {
const { doStart, uiActions, setup } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 1,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { id: '123', firstName: 'Sam', lastName: 'Tarley' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
// Final state should be that the panel is removed.
Rx.merge(container.getInput$(), container.getOutput$()).subscribe(() => {
if (
container.getInput().panels['123'] === undefined &&
container.getOutput().embeddableLoaded['123'] === undefined &&
container.getInput().panels['456'] !== undefined &&
container.getOutput().embeddableLoaded['456'] === true
) {
done();
}
});
container.updateInput({ panels: {} });
container.updateInput({
panels: {
'456': {
explicitInput: { id: '456' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
});
});

View file

@ -1,128 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { skip } from 'rxjs';
import { testPlugin } from './test_plugin';
import {
MockFilter,
FILTERABLE_EMBEDDABLE,
FilterableEmbeddableInput,
} from '../lib/test_samples/embeddables/filterable_embeddable';
import { FilterableEmbeddableFactory } from '../lib/test_samples/embeddables/filterable_embeddable_factory';
import { CONTACT_CARD_EMBEDDABLE } from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { SlowContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory';
import { HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddableFactoryDefinition } from './fixtures';
import { FilterableContainer } from '../lib/test_samples/embeddables/filterable_container';
import { isErrorEmbeddable } from '../lib';
import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world_container';
import { coreMock } from '@kbn/core/public/mocks';
const { setup, doStart, uiActions } = testPlugin(coreMock.createSetup(), coreMock.createStart());
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 2,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, factory);
setup.registerEmbeddableFactory(
HELLO_WORLD_EMBEDDABLE,
new HelloWorldEmbeddableFactoryDefinition()
);
const start = doStart();
test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const derivedFilter: MockFilter = {
$state: { store: 'appState' },
meta: { disabled: false, alias: 'name', negate: false },
query: { match: {} },
};
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [derivedFilter] },
start.getEmbeddableFactory
);
const embeddable = await container.addNewEmbeddable<any, any, any>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
embeddable.updateInput({ filters: undefined });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([
derivedFilter,
]);
});
test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async () => {
const container = new HelloWorldContainer(
{ id: 'hello', panels: {} },
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
const embeddable = await container.addNewEmbeddable<any, any, any>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
const subscription = await embeddable
.getInput$()
.pipe(skip(1))
.subscribe(() => {
if (embeddable.getInput().filters === undefined) {
subscription.unsubscribe();
}
});
embeddable.updateInput({ filters: undefined });
});
// The goal is to make sure that if the container input changes after `onPanelAdded` is called
// but before the embeddable factory returns the embeddable, that the `inheritedChildInput` and
// embeddable input comparisons won't cause explicit input to be set when it shouldn't.
test('Explicit input tests in async situations', (done: () => void) => {
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
}
);
container.updateInput({ lastName: 'lolol' });
const subscription = container.getOutput$().subscribe(() => {
if (container.getOutput().embeddableLoaded['123']) {
const embeddable = container.getChild<any>('123');
expect(embeddable).toBeDefined();
expect(embeddable.getInput().lastName).toBe('lolol');
subscription.unsubscribe();
done();
}
});
});

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { testPlugin } from './test_plugin';
import { FilterableContainerFactory } from '../lib/test_samples/embeddables/filterable_container_factory';
import { ContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory';
test('exports getEmbeddableFactories() function', () => {
const { doStart } = testPlugin();
expect(typeof doStart().getEmbeddableFactories).toBe('function');
});
test('returns empty list if there are no embeddable factories', () => {
const { doStart } = testPlugin();
const start = doStart();
const list = [...start.getEmbeddableFactories()];
expect(list).toEqual([]);
});
test('returns existing embeddable factories', () => {
const { setup, doStart } = testPlugin();
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).toBe(2);
expect(!!list.find(({ type }) => factory1.type === type)).toBe(true);
expect(!!list.find(({ type }) => factory2.type === type)).toBe(true);
});

View file

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { coreMock } from '@kbn/core/public/mocks';
import { ContainerInput, EmbeddableContainerSettings, isErrorEmbeddable } from '../lib';
import {
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
CONTACT_CARD_EMBEDDABLE,
FilterableEmbeddableFactory,
HelloWorldContainer,
SlowContactCardEmbeddableFactory,
} from '../lib/test_samples';
import { HelloWorldEmbeddableFactoryDefinition } from './fixtures';
import { testPlugin } from './test_plugin';
export async function createHelloWorldContainerAndEmbeddable(
containerInput: ContainerInput = { id: 'hello', panels: {} },
embeddableInput = {},
settings?: EmbeddableContainerSettings
) {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart);
const filterableFactory = new FilterableEmbeddableFactory();
const slowContactCardFactory = new SlowContactCardEmbeddableFactory({
execAction: uiActions.executeTriggerActions,
});
const contactCardCreateSpy = jest.spyOn(slowContactCardFactory, 'create');
const helloWorldFactory = new HelloWorldEmbeddableFactoryDefinition();
setup.registerEmbeddableFactory(filterableFactory.type, filterableFactory);
setup.registerEmbeddableFactory(slowContactCardFactory.type, slowContactCardFactory);
setup.registerEmbeddableFactory(helloWorldFactory.type, helloWorldFactory);
const start = doStart();
const container = new HelloWorldContainer(
containerInput,
{
getEmbeddableFactory: start.getEmbeddableFactory,
},
settings
);
const embeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, embeddableInput);
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
return {
setup,
start,
coreSetup,
coreStart,
container,
uiActions,
embeddable,
contactCardCreateSpy,
};
}
export const expectErrorAsync = (fn: (...args: unknown[]) => Promise<unknown>): Promise<Error> => {
return fn()
.then(() => {
throw new Error('Expected an error throw.');
})
.catch((error) => {
if (error.message === 'Expected an error throw.') {
throw error;
}
return error;
});
};
export const expectError = (fn: (...args: unknown[]) => unknown): Error => {
try {
fn();
throw new Error('Expected an error throw.');
} catch (error) {
if (error.message === 'Expected an error throw.') {
throw error;
}
return error;
}
};
export const of = async <T, P extends Promise<T>>(
promise: P
): Promise<[T | undefined, Error | unknown]> => {
try {
return [await promise, undefined];
} catch (error) {
return [, error];
}
};

View file

@ -10,7 +10,6 @@
"@kbn/saved-objects-plugin",
"@kbn/kibana-utils-plugin",
"@kbn/ui-actions-plugin",
"@kbn/i18n-react",
"@kbn/utility-types",
"@kbn/es-query",
"@kbn/i18n",

View file

@ -2826,9 +2826,6 @@
"embeddableApi.panelNotificationTrigger.title": "Notifications du panneau",
"embeddableApi.reactEmbeddable.factoryAlreadyExistsError": "Une usine incorporable pour le type : {key} est déjà enregistrée.",
"embeddableApi.reactEmbeddable.factoryNotFoundError": "Aucune usine incorporable n'a été trouvée pour le type : {key}",
"embeddableApi.samples.contactCard.displayName": "carte de visite",
"embeddableApi.samples.filterableContainer.displayName": "tableau de bord filtrable",
"embeddableApi.samples.filterableEmbeddable.displayName": "filtrable",
"embeddableApi.selectRangeTrigger.description": "Une plage de valeurs sur la visualisation",
"embeddableApi.selectRangeTrigger.title": "Sélection de la plage",
"embeddableApi.valueClickTrigger.description": "Un point de données cliquable sur la visualisation",

View file

@ -2821,9 +2821,6 @@
"embeddableApi.panelNotificationTrigger.title": "パネル通知",
"embeddableApi.reactEmbeddable.factoryAlreadyExistsError": "タイプ\"{key}\"の埋め込み可能ファクトリはすでに登録されています。",
"embeddableApi.reactEmbeddable.factoryNotFoundError": "タイプ\"{key}\"の埋め込み可能ファクトリが見つかりません",
"embeddableApi.samples.contactCard.displayName": "連絡先カード",
"embeddableApi.samples.filterableContainer.displayName": "フィルター可能なダッシュボード",
"embeddableApi.samples.filterableEmbeddable.displayName": "フィルター可能",
"embeddableApi.selectRangeTrigger.description": "ビジュアライゼーションでの値の範囲",
"embeddableApi.selectRangeTrigger.title": "範囲選択",
"embeddableApi.valueClickTrigger.description": "ビジュアライゼーションでデータポイントをクリック",

View file

@ -2811,9 +2811,6 @@
"embeddableApi.panelNotificationTrigger.title": "面板通知",
"embeddableApi.reactEmbeddable.factoryAlreadyExistsError": "已注册类型为 {key} 的可嵌入工厂。",
"embeddableApi.reactEmbeddable.factoryNotFoundError": "未找到类型为 {key} 的可嵌入工厂",
"embeddableApi.samples.contactCard.displayName": "联系卡片",
"embeddableApi.samples.filterableContainer.displayName": "可筛选仪表板",
"embeddableApi.samples.filterableEmbeddable.displayName": "可筛选",
"embeddableApi.selectRangeTrigger.description": "可视化上的值范围",
"embeddableApi.selectRangeTrigger.title": "范围选择",
"embeddableApi.valueClickTrigger.description": "可视化上的数据点单击",