mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
59d3ac65ec
commit
c59dddfa45
40 changed files with 14 additions and 3101 deletions
|
@ -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' },
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
|
@ -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 },
|
||||
});
|
||||
};
|
|
@ -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
|
||||
);
|
||||
});
|
|
@ -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() },
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
});
|
|
@ -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=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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() {}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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];
|
||||
}
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "ビジュアライゼーションでデータポイントをクリック",
|
||||
|
|
|
@ -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": "可视化上的数据点单击",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue