diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/__test__/get_sample_dashboard_input.ts b/src/legacy/core_plugins/dashboard_embeddable/public/__test__/get_sample_dashboard_input.ts new file mode 100644 index 0000000000000..50acd454b976c --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/__test__/get_sample_dashboard_input.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { QueryLanguageType, ViewMode } from 'plugins/embeddable_api/index'; +import { DashboardContainerInput, DashboardPanelState } from '../embeddable'; + +export function getSampleDashboardInput( + overrides?: Partial +): DashboardContainerInput { + return { + id: '123', + filters: [], + useMargins: false, + isFullScreenMode: false, + title: 'My Dashboard', + customization: {}, + query: { + language: QueryLanguageType.KUERY, + query: 'hi', + }, + timeRange: { + to: 'now', + from: 'now-15m', + }, + viewMode: ViewMode.VIEW, + panels: {}, + ...overrides, + }; +} + +export function getSampleDashboardPanel( + overrides: Partial & { embeddableId: string; type: string } +): DashboardPanelState { + return { + gridData: { + h: 15, + w: 15, + x: 0, + y: 0, + i: overrides.embeddableId, + }, + embeddableId: overrides.embeddableId, + type: overrides.type, + explicitInput: overrides.explicitInput || {}, + ...overrides, + }; +} diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/__test__/index.ts b/src/legacy/core_plugins/dashboard_embeddable/public/__test__/index.ts new file mode 100644 index 0000000000000..88909826971fc --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/__test__/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getSampleDashboardInput, getSampleDashboardPanel } from './get_sample_dashboard_input'; diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/actions/expand_panel_action.test.tsx b/src/legacy/core_plugins/dashboard_embeddable/public/actions/expand_panel_action.test.tsx new file mode 100644 index 0000000000000..398f512e7c5a7 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/actions/expand_panel_action.test.tsx @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +import { EmbeddableFactoryRegistry, isErrorEmbeddable } from 'plugins/embeddable_api/index'; +import { ExpandPanelAction } from './expand_panel_action'; +import { + HelloWorldEmbeddable, + HELLO_WORLD_EMBEDDABLE, + HelloWorldInput, + HelloWorldEmbeddableFactory, +} from 'plugins/embeddable_api/__test__/index'; +import { DashboardContainer } from '../embeddable'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../__test__'; + +const embeddableFactories = new EmbeddableFactoryRegistry(); +embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + +let container: DashboardContainer; +let embeddable: HelloWorldEmbeddable; + +beforeEach(async () => { + container = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + embeddableId: '123', + explicitInput: { firstName: 'Sam' }, + type: HELLO_WORLD_EMBEDDABLE, + }), + }, + }), + embeddableFactories + ); + + const helloEmbeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Kibana', + } + ); + + if (isErrorEmbeddable(helloEmbeddable)) { + throw new Error('Failed to create embeddable'); + } else { + embeddable = helloEmbeddable; + } +}); + +test('Sets the embeddable expanded panel id on the parent', async () => { + const expandPanelAction = new ExpandPanelAction(); + + expect(container.getInput().expandedPanelId).toBeUndefined(); + + expandPanelAction.execute({ embeddable }); + + expect(container.getInput().expandedPanelId).toBe(embeddable.id); +}); + +test('Is not compatible when embeddable is not in a dashboard container', async () => { + const action = new ExpandPanelAction(); + expect( + await action.isCompatible({ + embeddable: new HelloWorldEmbeddable({ firstName: 'sue', id: '123' }), + }) + ).toBe(false); +}); + +test('Execute throws an error when called with an embeddable not in a parent', async () => { + const action = new ExpandPanelAction(); + async function check() { + await action.execute({ embeddable: container }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test('Returns title', async () => { + const action = new ExpandPanelAction(); + expect(action.getTitle({ embeddable })).toBeDefined(); +}); + +test('Returns an icon', async () => { + const action = new ExpandPanelAction(); + expect(action.getIcon({ embeddable })).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/dashboard_container.test.tsx b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/dashboard_container.test.tsx new file mode 100644 index 0000000000000..9ce77a821dc33 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/dashboard_container.test.tsx @@ -0,0 +1,177 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { + HelloWorldEmbeddable, + HelloWorldInput, + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, +} from 'plugins/embeddable_api/__test__/embeddables'; +import { + EmbeddableFactoryRegistry, + isErrorEmbeddable, + ViewMode, + actionRegistry, + triggerRegistry, +} from 'plugins/embeddable_api/index'; +import { DashboardContainer } from './dashboard_container'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../__test__'; +import { EditModeAction } from 'plugins/embeddable_api/__test__/actions'; +import { mount } from 'enzyme'; +import { waitFor, nextTick } from 'test_utils/enzyme_helpers'; + +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddablePanel } from 'plugins/embeddable_api/panel'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CONTEXT_MENU_TRIGGER } from 'plugins/embeddable_api/triggers/trigger_registry'; + +test('DashboardContainer initializes embeddables', async done => { + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + const container = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + embeddableId: '123', + explicitInput: { firstName: 'Sam' }, + type: HELLO_WORLD_EMBEDDABLE, + }), + }, + }), + embeddableFactories + ); + + const subscription = container.getOutput$().subscribe(output => { + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + done(); + } + }); + + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + subscription.unsubscribe(); + done(); + } +}); + +test('DashboardContainer.addNewEmbeddable', async () => { + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + const container = new DashboardContainer(getSampleDashboardInput(), embeddableFactories); + const embeddable = await container.addNewEmbeddable(HELLO_WORLD_EMBEDDABLE, { + firstName: 'Kibana', + }); + expect(embeddable).toBeDefined(); + + if (!isErrorEmbeddable(embeddable)) { + expect(embeddable.getInput().firstName).toBe('Kibana'); + } else { + expect(false).toBe(true); + } + + const embeddableInContainer = container.getChild(embeddable.id); + expect(embeddableInContainer).toBeDefined(); + expect(embeddableInContainer.id).toBe(embeddable.id); +}); + +test('Container view mode change propagates to children', async () => { + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + const container = new DashboardContainer(getSampleDashboardInput(), embeddableFactories); + const embeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Bob', + } + ); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW); + + container.updateInput({ viewMode: ViewMode.EDIT }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); +}); + +test('Container in edit mode shows edit mode actions', async () => { + const editModeAction = new EditModeAction(); + actionRegistry.addAction(editModeAction); + triggerRegistry.attachAction({ + triggerId: CONTEXT_MENU_TRIGGER, + actionId: editModeAction.id, + }); + + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + const container = new DashboardContainer( + getSampleDashboardInput({ viewMode: ViewMode.VIEW }), + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Bob', + } + ); + + const component = mount( + + + + ); + + const button = findTestSubject(component, 'embeddablePanelToggleMenuIcon'); + + expect(button.length).toBe(1); + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + + expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1); + + const editAction = findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`); + + expect(editAction.length).toBe(0); + + container.updateInput({ viewMode: ViewMode.EDIT }); + await nextTick(); + component.update(); + expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(0); + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + await nextTick(); + component.update(); + expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(1); + + await nextTick(); + component.update(); + + const action = findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`); + expect(action.length).toBe(1); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/grid/__snapshots__/dashboard_grid.test.tsx.snap b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/grid/__snapshots__/dashboard_grid.test.tsx.snap new file mode 100644 index 0000000000000..0d95626652f87 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/grid/__snapshots__/dashboard_grid.test.tsx.snap @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DashboardGrid removes panel when removed from container 1`] = ` + +`; + +exports[`renders DashboardGrid 1`] = ` + +
+ +
+
+ +
+
+`; + +exports[`renders DashboardGrid with no visualizations 1`] = ` + +`; diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/grid/dashboard_grid.test.tsx b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/grid/dashboard_grid.test.tsx new file mode 100644 index 0000000000000..f3a9b595ee664 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/grid/dashboard_grid.test.tsx @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallowWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +// @ts-ignore +import sizeMe from 'react-sizeme'; + +import { ViewMode, EmbeddableFactoryRegistry } from 'plugins/embeddable_api/index'; + +import { DashboardGrid, DashboardGridProps } from './dashboard_grid'; +import { + HelloWorldEmbeddableFactory, + HELLO_WORLD_EMBEDDABLE, +} from 'plugins/embeddable_api/__test__'; +import { DashboardContainer } from '../dashboard_container'; +import { getSampleDashboardInput } from 'plugins/dashboard_embeddable/__test__'; + +jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true }); + +jest.mock( + 'ui/notify', + () => ({ + toastNotifications: { + addDanger: () => {}, + }, + }), + { virtual: true } +); + +let dashboardContainer: DashboardContainer | undefined; + +function getProps(props?: Partial): DashboardGridProps { + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + dashboardContainer = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '1': { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, + embeddableId: '1', + type: HELLO_WORLD_EMBEDDABLE, + explicitInput: { firstName: 'Bob' }, + }, + '2': { + gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' }, + type: HELLO_WORLD_EMBEDDABLE, + embeddableId: '2', + explicitInput: { firstName: 'Stacey' }, + }, + }, + }), + embeddableFactories + ); + const defaultTestProps: DashboardGridProps = { + embeddableFactories, + container: dashboardContainer, + intl: null as any, + }; + return Object.assign(defaultTestProps, props); +} + +beforeAll(() => { + // sizeme detects the width to be 0 in our test environment. noPlaceholder will mean that the grid contents will + // get rendered even when width is 0, which will improve our tests. + sizeMe.noPlaceholders = true; +}); + +afterAll(() => { + sizeMe.noPlaceholders = false; +}); + +test('renders DashboardGrid', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + const panelElements = component.find('InjectIntl(DashboardPanelUi)'); + expect(panelElements.length).toBe(2); +}); + +test('renders DashboardGrid with no visualizations', () => { + const props = getProps(); + props.container.updateInput({ panels: {} }); + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); +}); + +test('DashboardGrid removes panel when removed from container', async () => { + const props = getProps(); + const component = shallowWithIntl(); + const originalPanels = props.container.getInput().panels; + const filteredPanels = { ...originalPanels }; + delete filteredPanels['1']; + props.container.updateInput({ panels: filteredPanels }); + await nextTick(); + component.update(); + const panelElements = component.find('InjectIntl(DashboardPanelUi)'); + expect(panelElements.length).toBe(1); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/__snapshots__/dashboard_panel.test.tsx.snap b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/__snapshots__/dashboard_panel.test.tsx.snap new file mode 100644 index 0000000000000..9e17214552a4d --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/__snapshots__/dashboard_panel.test.tsx.snap @@ -0,0 +1,305 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DashboardPanel renders an embeddable when it is done loading 1`] = ` + +
+ Hello World! +
+ Sincerly, $ + +
+ , + "output": Object { + "birthLastName": undefined, + "birthName": "Sue", + "name": "Sue", + }, + "outputChangeListeners": Array [ + [Function], + ], + "panelContainer":
+
+
+
+
+
+ +
+
+
+
+
+ Hello World! +
+ Sincerly, $ + +
+
+
+
, + "parent": [Circular], + "parentChangesUnsubscribe": [Function], + "type": "hello_world", + "unsubscribe": [Function], + }, + }, + "id": "123", + "input": Object { + "filters": Array [], + "id": "123", + "isFullScreenMode": false, + "panels": Object { + "123": Object { + "embeddableId": "123", + "gridData": Object { + "h": 15, + "i": "123", + "w": 24, + "x": 0, + "y": 0, + }, + "partialInput": Object { + "firstName": "Sue", + "id": "123", + }, + "type": "hello_world", + }, + }, + "query": Object { + "language": "kuery", + "query": "", + }, + "timeRange": Object { + "from": "now-15m", + "to": "now", + }, + "title": "My Test Dashboard", + "useMargins": false, + "viewMode": "edit", + }, + "inputChangeListeners": Array [], + "isContainer": true, + "onExitFullScreenMode": [Function], + "onPanelsUpdated": [Function], + "output": Object { + "embeddableLoaded": Object { + "123": true, + }, + }, + "outputChangeListeners": Array [ + [Function], + ], + "panelContainer": undefined, + "parent": undefined, + "parentChangesUnsubscribe": undefined, + "type": "dashboard", + } + } + embeddableId="123" + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } +> +
+ +`; diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/__tests__/panel_state.ts b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/__tests__/panel_state.ts new file mode 100644 index 0000000000000..a074998183529 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/__tests__/panel_state.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { createPanelState } from '../panel_state'; + +function createPanelWithDimensions(x: number, y: number, w: number, h: number): PanelState { + return { + id: 'foo', + version: '6.3.0', + type: 'bar', + panelIndex: 'test', + title: 'test title', + gridData: { + x, + y, + w, + h, + i: 'an id', + }, + embeddableConfig: {}, + }; +} + +describe('Panel state', () => { + it('finds a spot on the right', () => { + // Default setup after a single panel, of default size, is on the grid + const panels = [createPanelWithDimensions(0, 0, 24, 30)]; + + const panel = createPanelState('1', 'a type', '1', panels); + expect(panel.gridData.x).to.equal(24); + expect(panel.gridData.y).to.equal(0); + }); + + it('finds a spot on the right when the panel is taller than any other panel on the grid', () => { + // Should be a little empty spot on the right. + const panels = [ + createPanelWithDimensions(0, 0, 24, 45), + createPanelWithDimensions(24, 0, 24, 30), + ]; + + const panel = createPanelState('1', 'a type', '1', panels); + expect(panel.gridData.x).to.equal(24); + expect(panel.gridData.y).to.equal(30); + }); + + it('finds an empty spot in the middle of the grid', () => { + const panels = [ + createPanelWithDimensions(0, 0, 48, 5), + createPanelWithDimensions(0, 5, 4, 30), + createPanelWithDimensions(40, 5, 4, 30), + createPanelWithDimensions(0, 55, 48, 5), + ]; + + const panel = createPanelState('1', 'a type', '1', panels); + expect(panel.gridData.x).to.equal(4); + expect(panel.gridData.y).to.equal(5); + }); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/create_panel_state.test.ts b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/create_panel_state.test.ts new file mode 100644 index 0000000000000..066ea1687a75c --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/create_panel_state.test.ts @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants'; +import { DashboardPanelState } from '../types'; +import { createPanelState } from './create_panel_state'; +import { HELLO_WORLD_EMBEDDABLE } from 'plugins/embeddable_api/__test__'; + +interface TestInput { + test: string; +} +const panels: DashboardPanelState[] = []; + +test('createPanelState adds a new panel state in 0,0 position', () => { + const panelState = createPanelState( + { embeddableId: '123', type: HELLO_WORLD_EMBEDDABLE, explicitInput: { test: 'hi' } }, + [] + ); + expect(panelState.explicitInput.test).toBe('hi'); + expect(panelState.type).toBe(HELLO_WORLD_EMBEDDABLE); + expect(panelState.embeddableId).toBeDefined(); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels.push(panelState); +}); + +test('createPanelState adds a second new panel state', () => { + const panelState = createPanelState( + { embeddableId: '456', type: HELLO_WORLD_EMBEDDABLE, explicitInput: { test: 'bye' } }, + panels + ); + expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels.push(panelState); +}); + +test('createPanelState adds a third new panel state', () => { + const panelState = createPanelState( + { embeddableId: '789', type: HELLO_WORLD_EMBEDDABLE, explicitInput: { test: 'bye' } }, + panels + ); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels.push(panelState); +}); + +test('createPanelState adds a new panel state in the top most position', () => { + const panelsWithEmptySpace = panels.filter(panel => panel.gridData.x === 0); + const panelState = createPanelState( + { embeddableId: '987', type: HELLO_WORLD_EMBEDDABLE, explicitInput: { test: 'bye' } }, + panelsWithEmptySpace + ); + expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/dashboard_panel.test.tsx b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/dashboard_panel.test.tsx new file mode 100644 index 0000000000000..7bf7a40d97a7f --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable/public/embeddable/panel/dashboard_panel.test.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { EuiLoadingChart } from '@elastic/eui'; +import { + HelloWorldEmbeddable, + HelloWorldInput, +} from 'plugins/embeddable_api/__test__/embeddables/hello_world_embeddable'; +import { embeddableFactories } from 'plugins/embeddable_api/index'; +import { QueryLanguageType, ViewMode } from 'plugins/embeddable_api/types'; +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DashboardContainer, DashboardContainerInput } from '../dashboard_container'; +import { DashboardPanel } from './dashboard_panel'; +import { + HELLO_WORLD_EMBEDDABLE, + FilterableEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from 'plugins/embeddable_api/__test__'; + +function getDashboardContainerInput(): DashboardContainerInput { + return { + id: '123', + viewMode: ViewMode.EDIT, + filters: [], + query: { + language: QueryLanguageType.KUERY, + query: '', + }, + timeRange: { + to: 'now', + from: 'now-15m', + }, + useMargins: false, + title: 'My Test Dashboard', + isFullScreenMode: false, + panels: {}, + }; +} + +test('DashboardPanel renders an embeddable when it is done loading', async () => { + const container = new DashboardContainer(getDashboardContainerInput(), embeddableFactories); + const newEmbeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Sue', + id: '123', + } + ); + + expect(newEmbeddable.id).toBeDefined(); + + const component = mountWithIntl( + + ); + + expect(component).toMatchSnapshot(); + + const loadingIndicator = component.find(EuiLoadingChart); + expect(loadingIndicator).toHaveLength(0); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/actions/edit_mode_action.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/edit_mode_action.ts new file mode 100644 index 0000000000000..fa8adefdbc689 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/edit_mode_action.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ViewMode } from 'plugins/embeddable_api/types'; +import { Action, ActionContext } from '../../actions'; + +export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION'; + +export class EditModeAction extends Action { + constructor() { + super(EDIT_MODE_ACTION); + } + + getTitle() { + return `I should only show up in edit mode`; + } + + isCompatible(context: ActionContext) { + return Promise.resolve(context.embeddable.getInput().viewMode === ViewMode.EDIT); + } + + execute() { + return; + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/actions/hello_world_action.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/hello_world_action.ts new file mode 100644 index 0000000000000..498b92f10b8c9 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/hello_world_action.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Action } from '../../actions'; + +export const HELLO_WORLD_ACTION = 'HELLO_WORLD_ACTION'; + +export class HelloWorldAction extends Action { + constructor() { + super(HELLO_WORLD_ACTION); + } + + getTitle() { + return 'Hello world!'; + } + + execute() { + alert('Hello world!'); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/actions/index.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/index.ts new file mode 100644 index 0000000000000..ea1d7a7590924 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { HelloWorldAction } from './hello_world_action'; +export { SayHelloAction } from './say_hello_action'; +export { EditModeAction } from './edit_mode_action'; +export { RestrictedAction } from './restricted_action'; diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/actions/restricted_action.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/restricted_action.ts new file mode 100644 index 0000000000000..a7c98e030dec4 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/restricted_action.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Action, ActionContext } from '../../actions'; + +export const RESTRICTED_ACTION = 'RESTRICTED_ACTION'; + +export class RestrictedAction extends Action { + private isCompatibleFn: (context: ActionContext) => boolean; + constructor(isCompatible: (context: ActionContext) => boolean) { + super(RESTRICTED_ACTION); + this.isCompatibleFn = isCompatible; + } + + getTitle() { + return `I am only sometimes compatible`; + } + + isCompatible(context: ActionContext) { + return Promise.resolve(this.isCompatibleFn(context)); + } + + execute() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/actions/say_hello_action.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/say_hello_action.ts new file mode 100644 index 0000000000000..96a05fb2b6e88 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/actions/say_hello_action.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Action, ActionContext, ExecuteActionContext } from '../../actions'; +import { EmbeddableInput, Embeddable, EmbeddableOutput } from '../../embeddables'; + +export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION'; + +interface SayHelloEmbeddableOutput extends EmbeddableOutput { + name: string; +} + +type SayHelloEmbeddable = Embeddable; + +export class SayHelloAction extends Action { + private sayHello: (name: string) => void; + constructor(sayHello: (name: string) => void) { + super(SAY_HELLO_ACTION); + this.sayHello = sayHello; + } + + getTitle() { + return 'Say hello'; + } + + isCompatible(context: ActionContext) { + return Promise.resolve(context.embeddable.getOutput().name !== undefined); + } + + execute(context: ExecuteActionContext) { + this.sayHello(`Hello, ${context.embeddable.getOutput().name}`); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/empty_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/empty_embeddable.tsx new file mode 100644 index 0000000000000..e6ae1c66799d4 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/empty_embeddable.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Embeddable, EmbeddableInput } from 'plugins/embeddable_api/index'; +import { EmbeddableOutput } from 'plugins/embeddable_api/embeddables'; + +export const EMPTY_EMBEDDABLE = 'EMPTY_EMBEDDABLE'; + +export class EmptyEmbeddable extends Embeddable { + constructor(initialInput: EmbeddableInput) { + super(EMPTY_EMBEDDABLE, initialInput, {}); + } + public render() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_container.tsx b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_container.tsx new file mode 100644 index 0000000000000..b3ec837bb3bc1 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_container.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { EmbeddableOutput, EmbeddableFactoryRegistry } from '../../embeddables'; +import { Filter } from '../../types'; +import { Container, ContainerInput } from '../../containers'; + +export const FILTERABLE_CONTAINER = 'FILTERABLE_CONTAINER'; + +export interface FilterableContainerInput extends ContainerInput { + filters: Filter[]; +} + +export interface InheritedChildrenInput { + filters: Filter[]; + id?: string; +} + +export class FilterableContainer extends Container< + InheritedChildrenInput, + FilterableContainerInput +> { + constructor( + initialInput: FilterableContainerInput, + embeddableFactories: EmbeddableFactoryRegistry, + parent?: Container + ) { + super( + FILTERABLE_CONTAINER, + initialInput, + { embeddableLoaded: {} }, + embeddableFactories, + parent + ); + } + + public getInheritedInput() { + return { + filters: this.input.filters, + }; + } + + public render() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_container_factory.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_container_factory.ts new file mode 100644 index 0000000000000..9ae97b2b30c96 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_container_factory.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { embeddableFactories, EmbeddableFactory } from 'plugins/embeddable_api/index'; +import { Container } from 'plugins/embeddable_api/containers'; +import { + FilterableContainer, + FilterableContainerInput, + FILTERABLE_CONTAINER, +} from './filterable_container'; + +export class FilterableContainerFactory extends EmbeddableFactory { + constructor() { + super({ + name: FILTERABLE_CONTAINER, + }); + } + + public getOutputSpec() { + return {}; + } + + public create(initialInput: FilterableContainerInput, parent?: Container) { + return Promise.resolve(new FilterableContainer(initialInput, embeddableFactories, parent)); + } +} + +embeddableFactories.registerFactory(new FilterableContainerFactory()); diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_embeddable.tsx new file mode 100644 index 0000000000000..f8b25e34330a1 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_embeddable.tsx @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Adapters } from 'ui/inspector'; +import { Container } from '../../containers'; +import { EmbeddableOutput, EmbeddableInput, Embeddable } from '../../embeddables'; +import { Filter } from '../../types'; + +export const FILTERABLE_EMBEDDABLE = 'FILTERABLE_EMBEDDABLE'; + +export interface FilterableEmbeddableInput extends EmbeddableInput { + filters: Filter[]; +} + +export class FilterableEmbeddable extends Embeddable { + constructor(initialInput: FilterableEmbeddableInput, parent?: Container) { + super(FILTERABLE_EMBEDDABLE, initialInput, {}, parent); + } + + public getInspectorAdapters() { + const inspectorAdapters: Adapters = { + filters: `My filters are ${JSON.stringify(this.input.filters)}`, + }; + return inspectorAdapters; + } + + public render() {} +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_embeddable_factory.ts new file mode 100644 index 0000000000000..5f0264f087d4a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/filterable_embeddable_factory.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + FilterableEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from './filterable_embeddable'; +import { + EmbeddableInput, + EmbeddableOutput, + embeddableFactories, + EmbeddableFactory, +} from '../../embeddables'; +import { Container, ContainerInput, ContainerOutput } from '../../containers'; + +export class FilterableEmbeddableFactory extends EmbeddableFactory { + constructor() { + super({ + name: FILTERABLE_EMBEDDABLE, + }); + } + + public getOutputSpec() { + return {}; + } + + public create< + CEI extends Partial = {}, + EO extends EmbeddableOutput = EmbeddableOutput, + CI extends ContainerInput = ContainerInput, + CO extends ContainerOutput = ContainerOutput + >(initialInput: FilterableEmbeddableInput, parent?: Container) { + return Promise.resolve(new FilterableEmbeddable(initialInput, parent)); + } +} + +embeddableFactories.registerFactory(new FilterableEmbeddableFactory()); diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_container.tsx b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_container.tsx new file mode 100644 index 0000000000000..851086969f4f2 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_container.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { EmbeddableFactoryRegistry } from 'plugins/embeddable_api/embeddables'; +import { Container } from 'plugins/embeddable_api/index'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { ViewMode } from 'plugins/embeddable_api/types'; +import { ContainerInput } from 'plugins/embeddable_api/containers'; +import { HelloWorldContainerComponent } from './hello_world_container_component'; + +export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER'; + +interface InheritedInput { + id: string; + viewMode: ViewMode; + lastName: string; +} + +export class HelloWorldContainer extends Container { + constructor(input: ContainerInput, embeddableFactories: EmbeddableFactoryRegistry) { + super(HELLO_WORLD_CONTAINER, input, { embeddableLoaded: {} }, embeddableFactories); + } + + public getInheritedInput(id: string) { + return { + id, + viewMode: this.input.viewMode || ViewMode.EDIT, + lastName: 'foo', + }; + } + + public render(node: HTMLElement) { + ReactDOM.render(, node); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_container_component.tsx b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_container_component.tsx new file mode 100644 index 0000000000000..02eba6e39c0ce --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_container_component.tsx @@ -0,0 +1,152 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Container, Embeddable } from 'plugins/embeddable_api/index'; +import React, { Component, RefObject } from 'react'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; + +interface Props { + container: Container; +} + +interface State { + embeddables: { [key: string]: Embeddable }; + loaded: { [key: string]: boolean }; +} + +export class HelloWorldContainerComponent extends Component { + private roots: { [key: string]: RefObject } = {}; + private mounted: boolean = false; + private inputSubscription?: Subscription; + private outputSubscription?: Subscription; + + public constructor(props: Props) { + super(props); + + Object.values(this.props.container.getInput().panels).forEach(panelState => { + this.roots[panelState.embeddableId] = React.createRef(); + }); + this.state = { + loaded: {}, + embeddables: {}, + }; + } + + public async componentDidMount() { + this.mounted = true; + + Object.values(this.props.container.getInput().panels).forEach(panelState => { + this.renderEmbeddable(panelState.embeddableId); + }); + + this.inputSubscription = this.props.container.getInput$().subscribe(() => { + Object.values(this.props.container.getInput().panels).forEach(async panelState => { + if (this.roots[panelState.embeddableId] === undefined) { + this.roots[panelState.embeddableId] = React.createRef(); + } + + if (this.state.embeddables[panelState.embeddableId] === undefined) { + const embeddable = await this.props.container.getChild(panelState.embeddableId); + const node = this.roots[panelState.embeddableId].current; + if (this.mounted && node !== null && embeddable) { + embeddable.renderInPanel(node, this.props.container); + + this.setState(prevState => ({ + loaded: { ...prevState.loaded, [panelState.embeddableId]: true }, + })); + } + } + }); + }); + + this.outputSubscription = this.props.container.getOutput$().subscribe(() => { + const embeddablesLoaded = this.props.container.getOutput().embeddableLoaded; + Object.keys(embeddablesLoaded).forEach(async id => { + const loaded = embeddablesLoaded[id]; + if (loaded && !this.state.loaded[id]) { + const embeddable = await this.props.container.getChild(id); + const node = this.roots[id].current; + if (this.mounted && node !== null && embeddable) { + embeddable.renderInPanel(node, this.props.container); + this.setState({ loaded: embeddablesLoaded }); + } + } + }); + }); + } + + public componentWillUnmount() { + this.props.container.destroy(); + + if (this.inputSubscription) { + this.inputSubscription.unsubscribe(); + } + if (this.outputSubscription) { + this.outputSubscription.unsubscribe(); + } + } + + public renderList() { + const list = Object.values(this.props.container.getInput().panels).map(panelState => { + const item = ( + +
+ + ); + return item; + }); + return list; + } + + public render() { + return ( +
+

HELLO WORLD! These are my precious embeddable children:

+ + {this.renderList()} +
+ ); + } + + private async renderEmbeddable(id: string) { + if (this.state.embeddables[id] !== undefined) { + return; + } + + if (this.roots[id] === undefined) { + this.roots[id] = React.createRef(); + } + + if (this.state.embeddables[id] === undefined) { + const embeddable = await this.props.container.getChild(id); + const node = this.roots[id].current; + if (this.mounted && node !== null && embeddable) { + embeddable.renderInPanel(node, this.props.container); + + this.setState(prevState => ({ + loaded: { ...prevState.loaded, [id]: true }, + embeddables: { + [id]: embeddable, + }, + })); + } + } + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable.tsx new file mode 100644 index 0000000000000..3dd55c56c2d89 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable.tsx @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Embeddable, EmbeddableInput } from 'plugins/embeddable_api/index'; +import React from 'react'; +import ReactDom from 'react-dom'; +import { EmbeddableOutput } from 'plugins/embeddable_api/embeddables'; +import { Container } from 'plugins/embeddable_api/containers'; +import { Subscription } from 'rxjs'; +import { HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable_factory'; +import { HelloWorldEmbeddableComponent } from './hello_world_embeddable_component'; + +export interface HelloWorldInput extends EmbeddableInput { + firstName: string; + lastName?: string; + nameTitle?: string; +} + +export interface HelloWorldOutput extends EmbeddableOutput { + name: string; + birthName: string; + birthLastName?: string; +} + +function getFullName(input: HelloWorldInput) { + const { nameTitle, firstName, lastName } = input; + const nameParts = [nameTitle, firstName, lastName].filter(name => name !== undefined); + return nameParts.join(' '); +} + +export class HelloWorldEmbeddable extends Embeddable { + private subscription: Subscription; + private node?: Element; + + constructor(initialInput: HelloWorldInput, parent?: Container) { + super( + HELLO_WORLD_EMBEDDABLE, + initialInput, + { + name: getFullName(initialInput), + birthLastName: initialInput.lastName, + birthName: initialInput.firstName, + }, + parent + ); + this.subscription = this.getInput$().subscribe(() => { + this.updateOutput({ + name: this.getFullName(), + title: this.input.title || this.getFullName(), + }); + }); + } + + public getFullName() { + return getFullName(this.input); + } + + public graduateWithPhd() { + this.updateInput({ nameTitle: 'Dr.' }); + } + + public getMarried(newLastName: string) { + this.updateInput({ lastName: newLastName }); + } + + public getDivorced() { + this.updateInput({ lastName: this.output.birthLastName }); + } + + public render(node: HTMLElement) { + this.node = node; + ReactDom.render(, node); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDom.unmountComponentAtNode(this.node); + } + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable_component.tsx b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable_component.tsx new file mode 100644 index 0000000000000..8b8cb0d7b9dbc --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable_component.tsx @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { Subscription } from 'rxjs'; +import { HelloWorldEmbeddable } from './hello_world_embeddable'; + +interface Props { + helloWorldEmbeddable: HelloWorldEmbeddable; +} + +interface State { + fullName: string; +} + +export class HelloWorldEmbeddableComponent extends React.Component { + private subscription?: Subscription; + private mounted: boolean = false; + + constructor(props: Props) { + super(props); + this.state = { + fullName: '', + }; + } + + componentDidMount() { + this.mounted = true; + this.subscription = this.props.helloWorldEmbeddable.getOutput$().subscribe(() => { + if (this.mounted) { + this.setState({ fullName: this.props.helloWorldEmbeddable.getOutput().name }); + } + }); + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + this.mounted = false; + } + + render() { + return ( +
+ Hello World! +
Sincerly, ${this.state.fullName} +
+ ); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable_factory.ts new file mode 100644 index 0000000000000..1c922abaff062 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/hello_world_embeddable_factory.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { embeddableFactories, EmbeddableFactory } from 'plugins/embeddable_api/index'; +import { Container } from 'plugins/embeddable_api/containers'; +import { HelloWorldEmbeddable, HelloWorldInput } from './hello_world_embeddable'; + +export const HELLO_WORLD_EMBEDDABLE = 'hello_world'; + +export class HelloWorldEmbeddableFactory extends EmbeddableFactory { + constructor() { + super({ + name: HELLO_WORLD_EMBEDDABLE, + }); + } + + public getOutputSpec() { + return {}; + } + + public create(initialInput: HelloWorldInput, parent?: Container) { + return Promise.resolve(new HelloWorldEmbeddable(initialInput, parent)); + } +} + +embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/index.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/index.ts new file mode 100644 index 0000000000000..34c6b8137429e --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/embeddables/index.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, +} from './hello_world_embeddable_factory'; +export { HelloWorldEmbeddable, HelloWorldInput } from './hello_world_embeddable'; +export { HelloWorldContainer } from './hello_world_container'; +export { EmptyEmbeddable } from './empty_embeddable'; +export { + FilterableEmbeddable, + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from './filterable_embeddable'; +export { + FilterableContainer, + FILTERABLE_CONTAINER, + FilterableContainerInput, +} from './filterable_container'; +export { FilterableEmbeddableFactory } from './filterable_embeddable_factory'; diff --git a/src/legacy/core_plugins/embeddable_api/public/__test__/index.ts b/src/legacy/core_plugins/embeddable_api/public/__test__/index.ts new file mode 100644 index 0000000000000..c9fc2b5cf98e5 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/__test__/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + EmptyEmbeddable, + HelloWorldEmbeddableFactory, + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddable, + HelloWorldInput, + HelloWorldContainer, + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + FILTERABLE_EMBEDDABLE, + FILTERABLE_CONTAINER, + FilterableContainerInput, + FilterableEmbeddableInput, +} from './embeddables'; + +export { SayHelloAction, EditModeAction, HelloWorldAction, RestrictedAction } from './actions'; diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/action.test.ts b/src/legacy/core_plugins/embeddable_api/public/actions/action.test.ts new file mode 100644 index 0000000000000..a06b396102607 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/action.test.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { HelloWorldAction, SayHelloAction, EmptyEmbeddable } from '../__test__/index'; + +test('SayHelloAction is not compatible with not matching embeddables', async () => { + const sayHelloAction = new SayHelloAction(() => {}); + const emptyEmbeddable = new EmptyEmbeddable({ id: '234' }); + + // @ts-ignore Typescript is nice and tells us ahead of time this is invalid, but + // I want to make sure it also returns false. + const isCompatible = await sayHelloAction.isCompatible({ embeddable: emptyEmbeddable }); + expect(isCompatible).toBe(false); +}); + +test('HelloWorldAction inherits isCompatible from base action', async () => { + const helloWorldAction = new HelloWorldAction(); + const emptyEmbeddable = new EmptyEmbeddable({ id: '234' }); + const isCompatible = await helloWorldAction.isCompatible({ embeddable: emptyEmbeddable }); + expect(isCompatible).toBe(true); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/action_registry.test.ts b/src/legacy/core_plugins/embeddable_api/public/actions/action_registry.test.ts new file mode 100644 index 0000000000000..4c1184b093b3b --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/action_registry.test.ts @@ -0,0 +1,158 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { + HelloWorldAction, + SayHelloAction, + EmptyEmbeddable, + RestrictedAction, +} from '../__test__/index'; +import { actionRegistry } from './action_registry'; +import { SAY_HELLO_ACTION } from '../__test__/actions/say_hello_action'; +import { triggerRegistry } from '../triggers'; +import { ActionContext } from './action'; +import { HELLO_WORLD_ACTION } from '../__test__/actions/hello_world_action'; + +beforeEach(() => { + actionRegistry.reset(); + triggerRegistry.reset(); +}); + +afterAll(() => { + actionRegistry.reset(); + triggerRegistry.reset(); +}); + +test('ActionRegistry adding and getting an action', async () => { + const sayHelloAction = new SayHelloAction(() => {}); + const helloWorldAction = new HelloWorldAction(); + + actionRegistry.addAction(sayHelloAction); + actionRegistry.addAction(helloWorldAction); + + expect(Object.keys(actionRegistry.getActions()).length).toBe(2); + + expect(actionRegistry.getAction(sayHelloAction.id)).toBe(sayHelloAction); + expect(actionRegistry.getAction(helloWorldAction.id)).toBe(helloWorldAction); +}); + +test('ActionRegistry removing an action', async () => { + const sayHelloAction = new SayHelloAction(() => {}); + const helloWorldAction = new HelloWorldAction(); + + actionRegistry.addAction(sayHelloAction); + actionRegistry.addAction(helloWorldAction); + actionRegistry.removeAction(sayHelloAction.id); + + expect(Object.keys(actionRegistry.getActions()).length).toBe(1); + + expect(actionRegistry.getAction(helloWorldAction.id)).toBe(helloWorldAction); +}); + +test(`ActionRegistry getting an action that doesn't exist returns undefined`, async () => { + expect(actionRegistry.getAction(SAY_HELLO_ACTION)).toBeUndefined(); +}); + +test(`Adding two actions with the same id throws an erro`, async () => { + expect(Object.keys(actionRegistry.getActions()).length).toBe(0); + const helloWorldAction = new HelloWorldAction(); + actionRegistry.addAction(helloWorldAction); + expect(() => actionRegistry.addAction(helloWorldAction)).toThrowError(); +}); + +test('getActionsForTrigger returns attached actions', async () => { + const embeddable = new EmptyEmbeddable({ id: '123' }); + const helloWorldAction = new HelloWorldAction(); + actionRegistry.addAction(helloWorldAction); + + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: [], + }; + triggerRegistry.registerTrigger(testTrigger); + + triggerRegistry.attachAction({ triggerId: 'MYTRIGGER', actionId: HELLO_WORLD_ACTION }); + + const moreActions = await actionRegistry.getActionsForTrigger('MYTRIGGER', { + embeddable, + }); + + expect(moreActions.length).toBe(1); +}); + +test('getActionsForTrigger filters out actions not applicable based on the context', async () => { + const action = new RestrictedAction((context: ActionContext) => { + return context.embeddable.id === 'accept'; + }); + actionRegistry.addAction(action); + const acceptEmbeddable = new EmptyEmbeddable({ id: 'accept' }); + const rejectEmbeddable = new EmptyEmbeddable({ id: 'reject' }); + + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: [action.id], + }; + triggerRegistry.registerTrigger(testTrigger); + + let actions = await actionRegistry.getActionsForTrigger(testTrigger.id, { + embeddable: acceptEmbeddable, + }); + + expect(actions.length).toBe(1); + + actions = await actionRegistry.getActionsForTrigger(testTrigger.id, { + embeddable: rejectEmbeddable, + }); + + expect(actions.length).toBe(0); +}); + +test(`getActionsForTrigger with an invalid trigger id throws an error`, async () => { + async function check() { + await actionRegistry.getActionsForTrigger('I do not exist', { + embeddable: new EmptyEmbeddable({ id: 'empty' }), + }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test(`getActionsForTrigger with a trigger mapping that maps to an non existant action throws an error`, async () => { + const testTrigger = { + id: '123', + title: '123', + actionIds: ['I do not exist'], + }; + triggerRegistry.registerTrigger(testTrigger); + + async function check() { + await actionRegistry.getActionsForTrigger('123', { + embeddable: new EmptyEmbeddable({ id: 'empty' }), + }); + } + await expect(check()).rejects.toThrow(Error); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts new file mode 100644 index 0000000000000..587701ba04645 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { + FilterableEmbeddable, + FilterableEmbeddableFactory, + FilterableContainer, + FILTERABLE_CONTAINER, + FilterableContainerInput, + FILTERABLE_EMBEDDABLE, + HelloWorldContainer, +} from '../__test__/index'; +import { ApplyFilterAction } from './apply_filter_action'; +import { embeddableFactories, isErrorEmbeddable } from '../embeddables'; +import { FilterableContainerFactory } from '../__test__/embeddables/filterable_container_factory'; +import { FilterableEmbeddableInput } from '../__test__/embeddables/filterable_embeddable'; + +beforeAll(() => { + embeddableFactories.registerFactory(new FilterableContainerFactory()); + embeddableFactories.registerFactory(new FilterableEmbeddableFactory()); +}); + +afterAll(() => { + embeddableFactories.reset(); +}); + +test('ApplyFilterAction applies the filter to the root of the container tree', async () => { + const applyFilterAction = new ApplyFilterAction(); + + const root = new FilterableContainer( + { id: 'root', panels: {}, filters: [] }, + embeddableFactories + ); + + const node1 = await root.addNewEmbeddable( + FILTERABLE_CONTAINER, + { panels: {}, id: 'node1' } + ); + + const node2 = await root.addNewEmbeddable( + FILTERABLE_CONTAINER, + { panels: {}, id: 'Node2' } + ); + + if (isErrorEmbeddable(node2) || isErrorEmbeddable(node1)) { + throw new Error(); + } + + const embeddable = await node2.addNewEmbeddable( + FILTERABLE_EMBEDDABLE, + { id: 'leaf' } + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error(); + } + + const filter = { + meta: { + disabled: false, + }, + query: { + field: 'bird:lark', + }, + }; + + applyFilterAction.execute({ embeddable, triggerContext: { filters: [filter] } }); + expect(root.getInput().filters.length).toBe(1); + expect(node1.getInput().filters.length).toBe(1); + expect(embeddable.getInput().filters.length).toBe(1); + expect(node2.getInput().filters.length).toBe(1); +}); + +test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { + const applyFilterAction = new ApplyFilterAction(); + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, embeddableFactories); + + const embeddable = await parent.addNewEmbeddable( + FILTERABLE_EMBEDDABLE, + { id: 'leaf' } + ); + + expect(await applyFilterAction.isCompatible({ embeddable })).toBe(false); +}); + +test('trying to execute on incompatible context throws an error ', async () => { + const applyFilterAction = new ApplyFilterAction(); + const parent = new HelloWorldContainer({ id: 'root', panels: {} }, embeddableFactories); + + const embeddable = await parent.addNewEmbeddable( + FILTERABLE_EMBEDDABLE, + { id: 'leaf' } + ); + async function check() { + await applyFilterAction.execute({ embeddable }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test('gets title', async () => { + const applyFilterAction = new ApplyFilterAction(); + expect(applyFilterAction.getTitle()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts new file mode 100644 index 0000000000000..a48abdd7e3ccb --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts @@ -0,0 +1,424 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); +import { skip } from 'rxjs/operators'; +import { + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, + HelloWorldContainer, + FilterableContainer, + FILTERABLE_EMBEDDABLE, + FilterableEmbeddableFactory, +} from '../__test__/index'; +import { EmbeddableFactoryRegistry, isErrorEmbeddable } from '../embeddables'; +import { + HelloWorldEmbeddable, + HelloWorldInput, + HelloWorldOutput, +} from '../__test__/embeddables/hello_world_embeddable'; +import { ContainerInput } from './container'; +import { ViewMode } from '../types'; +import { + FilterableEmbeddableInput, + FilterableEmbeddable, +} from '../__test__/embeddables/filterable_embeddable'; + +const embeddableFactories = new EmbeddableFactoryRegistry(); +embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); +embeddableFactories.registerFactory(new FilterableEmbeddableFactory()); + +async function creatHelloWorldContainerAndEmbeddable( + containerInput: ContainerInput = { id: 'hello', panels: {} }, + embeddableInput = {} +) { + const container = new HelloWorldContainer(containerInput, embeddableFactories); + const embeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + embeddableInput + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + return { container, embeddable }; +} + +test('Container initializes embeddables', async done => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + embeddableId: '123', + explicitInput: { name: 'Sam' }, + type: HELLO_WORLD_EMBEDDABLE, + }, + }, + }, + embeddableFactories + ); + + const subscription = container.getOutput$().subscribe(() => { + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + subscription.unsubscribe(); + done(); + } + }); + + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + done(); + } +}); + +test('Container.addNewEmbeddable', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { 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(embeddable.id); + expect(embeddableInContainer).toBeDefined(); + expect(embeddableInContainer.id).toBe(embeddable.id); +}); + +test('Container.removeEmbeddable removes and cleans up', async () => { + const container = new HelloWorldContainer( + { + id: 'hello', + panels: { + '123': { + embeddableId: '123', + explicitInput: { name: 'Sam' }, + type: HELLO_WORLD_EMBEDDABLE, + }, + }, + }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable< + HelloWorldInput, + HelloWorldOutput, + HelloWorldEmbeddable + >(HELLO_WORLD_EMBEDDABLE, { + firstName: 'Susy', + lastName: 'Q', + }); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + embeddable.getMarried('Z'); + + container.removeEmbeddable(embeddable.id); + + const noFind = container.getChild( + embeddable.id + ); + expect(noFind).toBeUndefined(); + + expect(container.getInput().panels[embeddable.id]).toBeUndefined(); + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + expect(embeddable.getDivorced).toThrowError(); + + expect(container.getOutput().embeddableLoaded[embeddable.id]).toBeUndefined(); +}); + +test('Container.input$ is notified when child embeddable input is updated', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {} }, + { + firstName: 'Susy', + lastName: 'Q', + } + ); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + const changes = jest.fn(); + const subscription = container.getInput$().subscribe(changes); + + embeddable.getMarried('Z'); + + expect(changes).toBeCalledTimes(2); + + expect(embeddable.getInput().lastName === 'Z'); + + embeddable.getDivorced(); + + expect(embeddable.getInput().lastName === 'Q'); + + expect(changes).toBeCalledTimes(3); + + subscription.unsubscribe(); + + embeddable.graduateWithPhd(); + + expect(changes).toBeCalledTimes(3); +}); + +test('Container.input$', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {} }, + { + firstName: 'Susy', + id: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + 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.graduateWithPhd(); + 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 creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {} }, + { + firstName: 'Susy', + id: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + expect(false).toBe(true); + return; + } + + 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.graduateWithPhd(); + expect(changes).toBeCalledTimes(2); + embeddable.graduateWithPhd(); + expect(changes).toBeCalledTimes(2); + subscription.unsubscribe(); +}); + +test('Container view mode change propagates to children', async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { 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 done => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {}, viewMode: ViewMode.VIEW }, + { + id: '123', + firstName: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + const inputSubscription = container.getInput$().subscribe(() => { + const newContainer = new HelloWorldContainer(container.getInput(), embeddableFactories); + const outputSubscription = newContainer.getOutput$().subscribe(output => { + if (output.embeddableLoaded[embeddable.id]) { + const newEmbeddable = newContainer.getChild(embeddable.id); + expect(newEmbeddable.getInput().nameTitle).toBe('Dr.'); + outputSubscription.unsubscribe(); + inputSubscription.unsubscribe(); + done(); + } + }); + }); + + embeddable.graduateWithPhd(); +}); + +test(`Derived container state passed to children`, async () => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {}, viewMode: ViewMode.VIEW }, + { + firstName: 'Susy', + } + ); + + let subscription = embeddable + .getInput$() + .pipe(skip(1)) + .subscribe((changes: Partial) => { + expect(changes.viewMode).toBe(ViewMode.EDIT); + }); + container.updateInput({ viewMode: ViewMode.EDIT }); + + subscription.unsubscribe(); + subscription = embeddable + .getInput$() + .pipe(skip(1)) + .subscribe((changes: Partial) => { + expect(changes.viewMode).toBe(ViewMode.VIEW); + }); + container.updateInput({ viewMode: ViewMode.VIEW }); + subscription.unsubscribe(); +}); + +test(`Can subscribe to children embeddable updates`, async done => { + const { embeddable } = await creatHelloWorldContainerAndEmbeddable( + { + id: 'hello container', + panels: {}, + viewMode: ViewMode.VIEW, + }, + { + firstName: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + const subscription = embeddable.getInput$().subscribe((input: HelloWorldInput) => { + if (input.nameTitle === 'Dr.') { + subscription.unsubscribe(); + done(); + } + }); + embeddable.graduateWithPhd(); +}); + +test('Test nested reactions', async done => { + const { container, embeddable } = await creatHelloWorldContainerAndEmbeddable( + { id: 'hello', panels: {}, viewMode: ViewMode.VIEW }, + { + firstName: 'Susy', + } + ); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + const containerSubscription = container.getInput$().subscribe(input => { + const embeddableNameTitle = embeddable.getInput().nameTitle; + const viewMode = input.viewMode; + const nameTitleFromContainer = container.getInputForChild(embeddable.id) + .nameTitle; + if ( + embeddableNameTitle === 'Dr.' && + nameTitleFromContainer === 'Dr.' && + viewMode === ViewMode.EDIT + ) { + containerSubscription.unsubscribe(); + embeddableSubscription.unsubscribe(); + done(); + } + }); + + const embeddableSubscription = embeddable.getInput$().subscribe(() => { + if (embeddable.getInput().nameTitle === 'Dr.') { + container.updateInput({ viewMode: ViewMode.EDIT }); + } + }); + + embeddable.graduateWithPhd(); +}); + +test('Explicit embeddable input mapped to undefined will default to inherited', async () => { + const derivedFilter = { + meta: { disabled: false }, + query: { query: 'name' }, + }; + const container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + const embeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, {}); + + if (isErrorEmbeddable(embeddable)) { + throw new Error('Error adding embeddable'); + } + + embeddable.updateInput({ filters: [] }); + + expect(container.getInputForChild(embeddable.id).filters).toEqual([]); + + embeddable.updateInput({ filters: undefined }); + + expect(container.getInputForChild(embeddable.id).filters).toEqual([ + derivedFilter, + ]); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.test.tsx b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.test.tsx new file mode 100644 index 0000000000000..a15af1279ce51 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.test.tsx @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { skip } from 'rxjs/operators'; +import { HelloWorldEmbeddable } from '../__test__/index'; + +test('Embeddable calls input subscribers when changed', async done => { + const hello = new HelloWorldEmbeddable({ id: '123', firstName: 'Sue' }); + + const subscription = hello + .getInput$() + .pipe(skip(1)) + .subscribe(input => { + expect(input.nameTitle).toEqual('Dr.'); + done(); + subscription.unsubscribe(); + }); + + hello.graduateWithPhd(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx new file mode 100644 index 0000000000000..0c0bbf80bb30c --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx @@ -0,0 +1,202 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { + HelloWorldEmbeddable, + HelloWorldInput, + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, + HelloWorldContainer, +} from 'plugins/embeddable_api/__test__/embeddables'; +import { + EmbeddableFactoryRegistry, + isErrorEmbeddable, + ViewMode, + actionRegistry, + triggerRegistry, +} from 'plugins/embeddable_api/index'; +import { EditModeAction } from 'plugins/embeddable_api/__test__/actions'; +import { mount } from 'enzyme'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddablePanel } from 'plugins/embeddable_api/panel'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CONTEXT_MENU_TRIGGER } from 'plugins/embeddable_api/triggers/trigger_registry'; + +const editModeAction = new EditModeAction(); +actionRegistry.addAction(editModeAction); +triggerRegistry.attachAction({ + triggerId: CONTEXT_MENU_TRIGGER, + actionId: editModeAction.id, +}); + +const embeddableFactories = new EmbeddableFactoryRegistry(); +embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + +test('HelloWorldContainer initializes embeddables', async done => { + const container = new HelloWorldContainer( + { + id: '123', + panels: { + '123': { + embeddableId: '123', + explicitInput: { firstName: 'Sam' }, + type: HELLO_WORLD_EMBEDDABLE, + }, + }, + }, + embeddableFactories + ); + + const subscription = container.getOutput$().subscribe(() => { + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + done(); + } + }); + + if (container.getOutput().embeddableLoaded['123']) { + const embeddable = container.getChild('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.id).toBe('123'); + subscription.unsubscribe(); + done(); + } +}); + +test('HelloWorldContainer.addNewEmbeddable', async () => { + const container = new HelloWorldContainer({ id: '123', panels: {} }, embeddableFactories); + const embeddable = await container.addNewEmbeddable(HELLO_WORLD_EMBEDDABLE, { + firstName: 'Kibana', + }); + expect(embeddable).toBeDefined(); + + if (!isErrorEmbeddable(embeddable)) { + expect(embeddable.getInput().firstName).toBe('Kibana'); + } else { + expect(false).toBe(true); + } + + const embeddableInContainer = container.getChild(embeddable.id); + expect(embeddableInContainer).toBeDefined(); + expect(embeddableInContainer.id).toBe(embeddable.id); +}); + +test('Container view mode change propagates to children', async () => { + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW }, + embeddableFactories + ); + const embeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Bob', + } + ); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW); + + container.updateInput({ viewMode: ViewMode.EDIT }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); +}); + +test('HelloWorldContainer in view mode hides edit mode actions', async () => { + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Bob', + } + ); + + const component = mount( + + + + ); + + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1); + await nextTick(); + component.update(); + expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0); +}); + +test('HelloWorldContainer in edit mode shows edit mode actions', async () => { + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW }, + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Bob', + } + ); + + const component = mount( + + + + ); + + const button = findTestSubject(component, 'embeddablePanelToggleMenuIcon'); + + expect(button.length).toBe(1); + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + + expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1); + await nextTick(); + component.update(); + expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0); + + container.updateInput({ viewMode: ViewMode.EDIT }); + await nextTick(); + component.update(); + + // Need to close and re-open to refresh. It doesn't update automatically. + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + await nextTick(); + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + await nextTick(); + expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(1); + + await nextTick(); + component.update(); + + const action = findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`); + expect(action.length).toBe(1); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/__snapshots__/add_panel_flyout.test.tsx.snap b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/__snapshots__/add_panel_flyout.test.tsx.snap new file mode 100644 index 0000000000000..10d887209a7dd --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/__snapshots__/add_panel_flyout.test.tsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshot 1`] = ` + + + +

+ +

+
+
+ + + + + + + + + , + "value": "createNew", + }, + Object { + "data-test-subj": "createNew-hello_world", + "inputDisplay": + + , + "value": "hello_world", + }, + ] + } + valueOfSelected="createNew" + /> + + + +
+`; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx new file mode 100644 index 0000000000000..14720fb11ad76 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +jest.mock('ui/new_platform', () => ({ + getNewPlatform: () => ({ + start: { + core: { + overlays: { + openFlyout: jest.fn(), + }, + }, + }, + }), +})); + +import { + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + HelloWorldEmbeddable, + FilterableContainerInput, +} from '../../../../__test__/index'; + +import { EmbeddableFactoryRegistry, isErrorEmbeddable } from '../../../../'; +import { AddPanelAction } from './add_panel_action'; +import { + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from 'plugins/embeddable_api/__test__/embeddables/filterable_embeddable'; +import { ViewMode } from 'plugins/embeddable_api/types'; + +const embeddableFactories = new EmbeddableFactoryRegistry(); +embeddableFactories.registerFactory(new FilterableEmbeddableFactory()); + +let container: FilterableContainer; +let embeddable: FilterableEmbeddable; + +beforeEach(async () => { + const derivedFilter = { + meta: { disabled: false }, + query: { query: 'name' }, + }; + container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + + const filterableEmbeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, { + id: '123', + }); + + if (isErrorEmbeddable(filterableEmbeddable)) { + throw new Error('Error creating new filterable embeddable'); + } else { + embeddable = filterableEmbeddable; + } +}); + +test('Is not compatible when container is in view mode', async () => { + const action = new AddPanelAction(); + container.updateInput({ viewMode: ViewMode.VIEW }); + expect(await action.isCompatible({ embeddable: container })).toBe(false); +}); + +test('Is not compatible when embeddable is not a container', async () => { + const action = new AddPanelAction(); + expect( + await action.isCompatible({ + embeddable, + }) + ).toBe(false); +}); + +test('Is compatible when embeddable is in a parent and in edit mode', async () => { + const action = new AddPanelAction(); + embeddable.updateInput({ viewMode: ViewMode.EDIT }); + expect(await action.isCompatible({ embeddable: container })).toBe(true); +}); + +test('Execute throws an error when called with an embeddable that is not a container', async () => { + const action = new AddPanelAction(); + async function check() { + await action.execute({ + // @ts-ignore + embeddable: new HelloWorldEmbeddable({ + firstName: 'sue', + id: '123', + viewMode: ViewMode.EDIT, + }), + }); + } + await expect(check()).rejects.toThrow(Error); +}); +test('Execute does not throw an error when called with a compatible container', async () => { + const action = new AddPanelAction(); + await action.execute({ + embeddable: container, + }); +}); + +test('Returns title', async () => { + const action = new AddPanelAction(); + expect(action.getTitle()).toBeDefined(); +}); + +test('Returns an icon', async () => { + const action = new AddPanelAction(); + expect(action.getIcon()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx new file mode 100644 index 0000000000000..56b12521abc18 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock( + 'ui/notify', + () => ({ + toastNotifications: { + addSuccess: () => {}, + }, + }), + { virtual: true } +); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +import React from 'react'; +import { + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, + HelloWorldContainer, +} from '../../../../__test__/index'; + +import { AddPanelFlyout } from './add_panel_flyout'; +import { Container } from 'plugins/embeddable_api/containers'; +import { EmbeddableFactoryRegistry } from 'plugins/embeddable_api/embeddables'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { skip } from 'rxjs/operators'; + +const onClose = jest.fn(); +let container: Container; + +function createHelloWorldContainer(input = { id: '123', panels: {} }) { + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + return new HelloWorldContainer(input, embeddableFactories); +} + +beforeEach(() => { + container = createHelloWorldContainer(); +}); + +test('matches snapshot', async () => { + const component = shallowWithIntl(); + + expect(component).toMatchSnapshot(); +}); + +test('adds a panel to the container', async done => { + const component = mountWithIntl(); + + expect(Object.values(container.getInput().panels).length).toBe(0); + + const subscription = container + .getInput$() + .pipe(skip(1)) + .subscribe(input => { + expect(input.panels).toBeDefined(); + if (input.panels) { + expect(Object.values(input.panels).length).toBe(1); + subscription.unsubscribe(); + } + done(); + }); + + findTestSubject(component, 'createNew').simulate('click'); + findTestSubject(component, `createNew-${HELLO_WORLD_EMBEDDABLE}`).simulate('click'); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/__snapshots__/customize_panel_flyout.test.tsx.snap b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/__snapshots__/customize_panel_flyout.test.tsx.snap new file mode 100644 index 0000000000000..ba8d56b8bffbe --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/__snapshots__/customize_panel_flyout.test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshot 1`] = ` + + + +

+ Joe foo +

+
+
+ + + + Save & Close + + +
+`; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts new file mode 100644 index 0000000000000..fd5654751fc2a --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +import { + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, + HelloWorldContainer, + HelloWorldEmbeddable, + HelloWorldInput, +} from '../../../../__test__/index'; + +import { Container } from 'plugins/embeddable_api/containers'; +import { EmbeddableFactoryRegistry, isErrorEmbeddable } from 'plugins/embeddable_api/embeddables'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { nextTick } from 'test_utils/enzyme_helpers'; +import { CustomizePanelTitleAction } from './customize_panel_action'; + +let container: Container; +let embeddable: HelloWorldEmbeddable; + +function createHelloWorldContainer(input = { id: '123', panels: {} }) { + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + return new HelloWorldContainer(input, embeddableFactories); +} + +beforeEach(async () => { + container = createHelloWorldContainer(); + const helloEmbeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + id: 'joe', + firstName: 'Joe', + } + ); + if (isErrorEmbeddable(helloEmbeddable)) { + throw new Error('Error creating new hello world embeddable'); + } else { + embeddable = helloEmbeddable; + } +}); + +test('Updates the embeddable title when given', async done => { + const getUserData = () => Promise.resolve({ title: 'What is up?' }); + const customizePanelAction = new CustomizePanelTitleAction(getUserData); + expect(embeddable.getInput().title).toBeUndefined(); + await customizePanelAction.execute({ embeddable, container }); + await nextTick(); + expect(embeddable.getInput().title).toBe('What is up?'); + + // Recreating the container should preserve the custom title. + const containerClone = createHelloWorldContainer(container.getInput()); + // Need to wait for the container to tell us the embeddable has been loaded. + const subscription = containerClone.getOutput$().subscribe(() => { + if (containerClone.getOutput().embeddableLoaded[embeddable.id]) { + expect(embeddable.getInput().title).toBe('What is up?'); + subscription.unsubscribe(); + done(); + } + }); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_flyout.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_flyout.test.tsx new file mode 100644 index 0000000000000..49085ab96347e --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_flyout.test.tsx @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +import React from 'react'; +import { + HELLO_WORLD_EMBEDDABLE, + HelloWorldEmbeddableFactory, + HelloWorldContainer, + HelloWorldEmbeddable, + HelloWorldInput, +} from '../../../../__test__/index'; + +import { CustomizePanelFlyout } from './customize_panel_flyout'; +import { Container } from 'plugins/embeddable_api/containers'; +import { EmbeddableFactoryRegistry, isErrorEmbeddable } from 'plugins/embeddable_api/embeddables'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; + +let container: Container; +let embeddable: HelloWorldEmbeddable; + +beforeEach(async () => { + const embeddableFactories = new EmbeddableFactoryRegistry(); + embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); + container = new HelloWorldContainer({ id: '123', panels: {} }, embeddableFactories); + const helloEmbeddable = await container.addNewEmbeddable( + HELLO_WORLD_EMBEDDABLE, + { + firstName: 'Joe', + } + ); + if (isErrorEmbeddable(helloEmbeddable)) { + throw new Error('Error creating new hello world embeddable'); + } else { + embeddable = helloEmbeddable; + } +}); + +test('matches snapshot', async () => { + const component = shallowWithIntl( + {}} /> + ); + + expect(component).toMatchSnapshot(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/get_edit_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/get_edit_panel_action.test.tsx new file mode 100644 index 0000000000000..ae57988704e3d --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/get_edit_panel_action.test.tsx @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +import { getEditPanelAction } from './get_edit_panel_action'; +import { Embeddable, EmbeddableInput } from 'plugins/embeddable_api/embeddables'; +import { HelloWorldEmbeddable } from 'plugins/embeddable_api/__test__'; +import { ViewMode } from 'plugins/embeddable_api/types'; + +class EditableEmbeddable extends Embeddable { + constructor(input: EmbeddableInput, editable: boolean) { + super('EDITABLE_EMBEDDABLE', input, { + editUrl: 'www.google.com', + editable, + }); + } +} + +test('is visible when edit url is available, in edit mode and editable', async () => { + const action = getEditPanelAction(); + expect( + action.isVisible({ + embeddable: new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true), + }) + ).toBe(true); +}); + +test('is enabled when edit url is available and in edit mode', async () => { + const action = getEditPanelAction(); + expect( + action.isDisabled({ + embeddable: new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true), + }) + ).toBe(false); +}); + +test('getHref returns the edit urls', async () => { + const action = getEditPanelAction(); + expect(action.getHref).toBeDefined(); + + if (action.getHref) { + const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true); + expect( + action.getHref({ + embeddable, + }) + ).toBe(embeddable.getOutput().editUrl); + } +}); + +test('is not visible when edit url is not available', async () => { + const action = getEditPanelAction(); + expect( + action.isVisible({ + embeddable: new HelloWorldEmbeddable({ + id: '123', + firstName: 'sue', + viewMode: ViewMode.EDIT, + }), + }) + ).toBe(false); +}); + +test('is disabled when edit url is not available', async () => { + const action = getEditPanelAction(); + expect( + action.isDisabled({ + embeddable: new HelloWorldEmbeddable({ + id: '123', + firstName: 'sue', + viewMode: ViewMode.EDIT, + }), + }) + ).toBe(true); +}); + +test('is not visible when edit url is available but in view mode', async () => { + const action = getEditPanelAction(); + expect( + action.isVisible({ + embeddable: new EditableEmbeddable( + { + id: '123', + viewMode: ViewMode.VIEW, + }, + true + ), + }) + ).toBe(false); +}); + +test('is not visible when edit url is available, in edit mode, but not editable', async () => { + const action = getEditPanelAction(); + expect( + action.isVisible({ + embeddable: new EditableEmbeddable( + { + id: '123', + viewMode: ViewMode.EDIT, + }, + false + ), + }) + ).toBe(false); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx new file mode 100644 index 0000000000000..07d8a6b4bbffe --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +jest.mock('ui/inspector', () => ({ + Inspector: { + open: jest.fn(() => ({ + onClose: Promise.resolve(), + })), + isAvailable: (adapters: Adapters) => { + return Boolean(adapters); + }, + }, +})); + +import { + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + HelloWorldEmbeddable, +} from '../../../__test__/index'; + +import { EmbeddableFactoryRegistry, isErrorEmbeddable } from '../../..'; +import { InspectPanelAction } from './inspect_panel_action'; +import { + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from 'plugins/embeddable_api/__test__/embeddables/filterable_embeddable'; +import { Inspector, Adapters } from 'ui/inspector'; + +const embeddableFactories = new EmbeddableFactoryRegistry(); +embeddableFactories.registerFactory(new FilterableEmbeddableFactory()); + +let container: FilterableContainer; +let embeddable: FilterableEmbeddable; + +beforeEach(async () => { + const derivedFilter = { + meta: { disabled: false }, + query: { query: 'name' }, + }; + container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + + const filterableEmbeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, { + id: '123', + }); + + if (isErrorEmbeddable(filterableEmbeddable)) { + throw new Error('Error creating new filterable embeddable'); + } else { + embeddable = filterableEmbeddable; + } +}); + +test('Is compatible when inspector adapters are available', async () => { + const inspectAction = new InspectPanelAction(); + expect(await inspectAction.isCompatible({ embeddable })).toBe(true); +}); + +test('Is not compatible when inspector adapters are not available', async () => { + const inspectAction = new InspectPanelAction(); + expect( + await inspectAction.isCompatible({ + embeddable: new HelloWorldEmbeddable({ firstName: 'sue', id: '123' }), + }) + ).toBe(false); +}); + +test('Executes when inspector adapters are available', async () => { + const inspectAction = new InspectPanelAction(); + await inspectAction.execute({ embeddable }); + expect(Inspector.open).toBeCalled(); +}); + +test('Execute throws an error when inspector adapters are not available', async () => { + const inspectAction = new InspectPanelAction(); + await inspectAction.execute({ embeddable }); + + await expect( + inspectAction.execute({ + embeddable: new HelloWorldEmbeddable({ firstName: 'sue', id: '123' }), + }) + ).rejects.toThrow(Error); +}); + +test('Returns title', async () => { + const inspectAction = new InspectPanelAction(); + expect(inspectAction.getTitle()).toBeDefined(); +}); + +test('Returns an icon', async () => { + const inspectAction = new InspectPanelAction(); + expect(inspectAction.getIcon()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx new file mode 100644 index 0000000000000..0019c6c688ea8 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +import { + FilterableContainer, + FilterableEmbeddable, + FilterableEmbeddableFactory, + HelloWorldEmbeddable, +} from '../../../__test__/index'; + +import { EmbeddableFactoryRegistry, isErrorEmbeddable } from '../../../'; +import { RemovePanelAction } from './remove_panel_action'; +import { + FilterableEmbeddableInput, + FILTERABLE_EMBEDDABLE, +} from 'plugins/embeddable_api/__test__/embeddables/filterable_embeddable'; + +const embeddableFactories = new EmbeddableFactoryRegistry(); +embeddableFactories.registerFactory(new FilterableEmbeddableFactory()); + +let container: FilterableContainer; +let embeddable: FilterableEmbeddable; + +beforeEach(async () => { + const derivedFilter = { + meta: { disabled: false }, + query: { query: 'name' }, + }; + container = new FilterableContainer( + { id: 'hello', panels: {}, filters: [derivedFilter] }, + embeddableFactories + ); + + const filterableEmbeddable = await container.addNewEmbeddable< + FilterableEmbeddableInput, + FilterableEmbeddable + >(FILTERABLE_EMBEDDABLE, { + id: '123', + }); + + if (isErrorEmbeddable(filterableEmbeddable)) { + throw new Error('Error creating new filterable embeddable'); + } else { + embeddable = filterableEmbeddable; + } +}); + +test('Removes the embeddable', async () => { + const removePanelAction = new RemovePanelAction(); + expect(container.getChild(embeddable.id)).toBeDefined(); + + removePanelAction.execute({ embeddable }); + + expect(container.getChild(embeddable.id)).toBeUndefined(); +}); + +test('Is not compatible when embeddable is not in a parent', async () => { + const action = new RemovePanelAction(); + expect( + await action.isCompatible({ + embeddable: new HelloWorldEmbeddable({ firstName: 'sue', id: '123' }), + }) + ).toBe(false); +}); + +test('Execute throws an error when called with an embeddable not in a parent', async () => { + const action = new RemovePanelAction(); + async function check() { + await action.execute({ embeddable: container }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test('Returns title', async () => { + const action = new RemovePanelAction(); + expect(action.getTitle()).toBeDefined(); +}); + +test('Returns an icon', async () => { + const action = new RemovePanelAction(); + expect(action.getIcon()).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts new file mode 100644 index 0000000000000..a98847efabe82 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts @@ -0,0 +1,145 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; + +const executeFn = jest.fn(); + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.mock('../context_menu_actions/open_context_menu', () => ({ + openContextMenu: (actions: EuiContextMenuPanelDescriptor[]) => jest.fn()(actions), +})); + +import { triggerRegistry } from '../triggers'; +import { Action, ExecuteActionContext, actionRegistry } from '../actions'; +import { executeTriggerActions } from './execute_trigger_actions'; +import { HelloWorldEmbeddable } from '../__test__'; + +class TestAction extends Action { + public checkCompatibility: (context: ExecuteActionContext) => boolean; + + constructor(id: string, checkCompatibility: (context: ExecuteActionContext) => boolean) { + super(id); + this.checkCompatibility = checkCompatibility; + } + + public getTitle() { + return 'test'; + } + + isCompatible(context: ExecuteActionContext) { + return Promise.resolve(this.checkCompatibility(context)); + } + + execute(context: ExecuteActionContext) { + executeFn(context); + } +} + +beforeEach(() => { + triggerRegistry.reset(); + actionRegistry.reset(); + executeFn.mockReset(); +}); + +afterAll(() => { + triggerRegistry.reset(); +}); + +test('executeTriggerActions executes a single action mapped to a trigger', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['test1'], + }; + triggerRegistry.registerTrigger(testTrigger); + + actionRegistry.addAction(new TestAction('test1', () => true)); + + const context = { + embeddable: new HelloWorldEmbeddable({ id: '123', firstName: 'Stacey' }), + triggerContext: {}, + }; + + await executeTriggerActions('MYTRIGGER', context); + + expect(executeFn).toBeCalledTimes(1); + expect(executeFn).toBeCalledWith(context); +}); + +test('executeTriggerActions throws an error if the action id does not exist', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['testaction'], + }; + triggerRegistry.registerTrigger(testTrigger); + + const context = { + embeddable: new HelloWorldEmbeddable({ id: '123', firstName: 'Stacey' }), + triggerContext: {}, + }; + await expect(executeTriggerActions('MYTRIGGER', context)).rejects.toThrowError(); +}); + +test('executeTriggerActions does not execute an incompatible action', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['test1'], + }; + triggerRegistry.registerTrigger(testTrigger); + + actionRegistry.addAction( + new TestAction('test1', ({ embeddable }) => embeddable.id === 'executeme') + ); + + const context = { + embeddable: new HelloWorldEmbeddable({ id: 'executeme', firstName: 'Stacey' }), + triggerContext: {}, + }; + + await executeTriggerActions('MYTRIGGER', context); + expect(executeFn).toBeCalledTimes(1); +}); + +test('executeTriggerActions shows a context menu when more than one action is mapped to a trigger', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['test1', 'test2'], + }; + triggerRegistry.registerTrigger(testTrigger); + + actionRegistry.addAction(new TestAction('test1', () => true)); + actionRegistry.addAction(new TestAction('test2', () => true)); + + const context = { + embeddable: new HelloWorldEmbeddable({ id: 'executeme', firstName: 'Stacey' }), + triggerContext: {}, + }; + + await executeTriggerActions('MYTRIGGER', context); + expect(executeFn).toBeCalledTimes(0); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts new file mode 100644 index 0000000000000..323856d99d838 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +import { triggerRegistry } from '../triggers'; +import { HELLO_WORLD_ACTION } from '../__test__/actions/hello_world_action'; + +beforeAll(() => { + triggerRegistry.reset(); +}); + +afterAll(() => { + triggerRegistry.reset(); +}); + +test('TriggerRegistry adding and getting a new trigger', async () => { + const testTrigger = { + id: 'MYTRIGGER', + title: 'My trigger', + actionIds: ['123'], + }; + triggerRegistry.registerTrigger(testTrigger); + + expect(triggerRegistry.getTrigger('MYTRIGGER')).toBe(testTrigger); +}); + +test('TriggerRegistry attach a trigger to an action', async () => { + triggerRegistry.attachAction({ triggerId: 'MYTRIGGER', actionId: HELLO_WORLD_ACTION }); + const trigger = triggerRegistry.getTrigger('MYTRIGGER'); + expect(trigger).toBeDefined(); + if (trigger) { + expect(trigger.actionIds).toEqual(['123', HELLO_WORLD_ACTION]); + } +}); + +test('TriggerRegistry dettach a trigger from an action', async () => { + triggerRegistry.detachAction({ triggerId: 'MYTRIGGER', actionId: HELLO_WORLD_ACTION }); + const trigger = triggerRegistry.getTrigger('MYTRIGGER'); + expect(trigger).toBeDefined(); + if (trigger) { + expect(trigger.actionIds).toEqual(['123']); + } +}); + +test('TriggerRegistry dettach an invalid trigger from an action throws an error', async () => { + expect(() => + triggerRegistry.detachAction({ triggerId: 'i do not exist', actionId: HELLO_WORLD_ACTION }) + ).toThrowError(); +}); + +test('TriggerRegistry attach an invalid trigger from an action throws an error', async () => { + expect(() => + triggerRegistry.attachAction({ triggerId: 'i do not exist', actionId: HELLO_WORLD_ACTION }) + ).toThrowError(); +}); diff --git a/src/test_utils/public/enzyme_helpers.tsx b/src/test_utils/public/enzyme_helpers.tsx index f73b4b55d159a..0b66253716561 100644 --- a/src/test_utils/public/enzyme_helpers.tsx +++ b/src/test_utils/public/enzyme_helpers.tsx @@ -126,3 +126,31 @@ export function renderWithIntl( return render(nodeWithIntlProp(node), options); } + +export const nextTick = () => new Promise(res => process.nextTick(res)); +const MAX_WAIT = 30; +export const waitFor = async (fn: () => void, wrapper?: ReactWrapper) => { + console.log('in waitFor'); + await nextTick(); + if (wrapper) { + wrapper.update(); + } + let errorCount = 0; + while (true) { + try { + fn(); + return; + } catch (e) { + errorCount++; + console.log('Error: ', e); + await nextTick(); + if (wrapper) { + wrapper.update(); + } + if (errorCount > MAX_WAIT) { + throw new Error(e); + return; + } + } + } +}; diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 6b689d78f992b..f76d2bc134fef 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -34,6 +34,7 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/embedding_visualizations'), require.resolve('./test_suites/panel_actions'), require.resolve('./test_suites/core_plugins'), + require.resolve('./test_suites/embeddable_explorer'), ], services: functionalConfig.get('services'), pageObjects: functionalConfig.get('pageObjects'), diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts new file mode 100644 index 0000000000000..f2b82013de4a1 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Legacy } from 'kibana'; +import { Plugin as EmbeddableExplorer } from './plugin'; +import { createShim } from './shim'; + +export type CoreShim = object; + +// eslint-disable-next-line import/no-default-export +export default function(kibana: any) { + return new kibana.Plugin({ + require: ['kibana'], + uiExports: { + app: { + title: 'Embeddable Explorer', + order: 1, + main: 'plugins/kbn_tp_embeddable_explorer', + }, + embeddableActions: [ + 'plugins/kbn_tp_embeddable_explorer/actions/hello_world_action', + 'plugins/kbn_tp_embeddable_explorer/actions/edit_mode_action', + ], + embeddableFactories: [ + 'plugins/kbn_tp_embeddable_explorer/embeddables/hello_world_embeddable_factory', + // 'plugins/kbn_tp_embeddable_explorer/embeddables/button_embeddable_factory', + ], + }, + init(server: Legacy.Server) { + const embeddableExplorer = new EmbeddableExplorer(server); + embeddableExplorer.start(createShim()); + + // @ts-ignore + server.injectUiAppVars('kbn_tp_embeddable_explorer', async () => + server.getInjectedUiAppVars('kibana') + ); + }, + }); +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json new file mode 100644 index 0000000000000..84e1b391a515e --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -0,0 +1,22 @@ +{ + "name": "kbn_tp_embeddable_explorer", + "version": "1.0.0", + "main":"target/test/plugin_functional/plugins/kbn_tp_embeddable_explorer", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "dependencies": { + "@elastic/eui": "9.9.0", + "react": "^16.8.0" + }, + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "@kbn/plugin-helpers" : "9.0.2", + "typescript": "^3.3.3333" + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/plugin.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/plugin.ts new file mode 100644 index 0000000000000..c31e222d49c0c --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/plugin.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Legacy } from 'kibana'; +import { CoreShim } from '.'; + +export class Plugin { + public server: Legacy.Server; + + constructor(server: Legacy.Server) { + this.server = server; + } + + public start({ core }: { core: CoreShim }): void { + return; + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/actions/edit_mode_action.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/actions/edit_mode_action.tsx new file mode 100644 index 0000000000000..2415d8b096709 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/actions/edit_mode_action.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { actionRegistry, triggerRegistry } from 'plugins/embeddable_api/index'; +import { EditModeAction } from 'plugins/embeddable_api/__test__'; +import { CONTEXT_MENU_TRIGGER } from 'plugins/embeddable_api/triggers/trigger_registry'; + +const editModeAction = new EditModeAction(); +actionRegistry.addAction(editModeAction); +triggerRegistry.attachAction({ + triggerId: CONTEXT_MENU_TRIGGER, + actionId: editModeAction.id, +}); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/actions/hello_world_action.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/actions/hello_world_action.tsx new file mode 100644 index 0000000000000..9720255fabe19 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/actions/hello_world_action.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Action, actionRegistry, triggerRegistry } from 'plugins/embeddable_api/index'; + +import { EuiFlyout } from '@elastic/eui'; +import { CONTEXT_MENU_TRIGGER } from 'plugins/embeddable_api/index'; +import React from 'react'; + +import { getNewPlatform } from 'ui/new_platform'; +import { FlyoutRef } from '../../../../../../src/core/public'; + +const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID'; + +export class HelloWorldAction extends Action { + private flyoutSession: FlyoutRef | undefined; + constructor() { + super(HELLO_WORLD_ACTION_ID); + } + + public getTitle() { + return 'Hello World!'; + } + + public execute() { + this.flyoutSession = getNewPlatform().setup.core.overlays.openFlyout( + this.flyoutSession && this.flyoutSession.close()}> + Hello World! + , + { + 'data-test-subj': 'helloWorldAction', + } + ); + } +} + +actionRegistry.addAction(new HelloWorldAction()); + +triggerRegistry.attachAction({ + triggerId: CONTEXT_MENU_TRIGGER, + actionId: HELLO_WORLD_ACTION_ID, +}); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx new file mode 100644 index 0000000000000..5a1f252b1d27e --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiTab } from '@elastic/eui'; +import { EmbeddableFactoryRegistry } from 'plugins/embeddable_api/index'; +import React, { Component } from 'react'; +import { DashboardContainerExample } from './dashboard_container_example'; +import { HelloWorldEmbeddableExample } from './hello_world_embeddable_example'; +import { ListContainerExample } from './list_container_example'; +import { VisualizeEmbeddableExample } from './visualize_embeddable_example'; + +export interface AppProps { + embeddableFactories: EmbeddableFactoryRegistry; +} + +export class App extends Component { + private tabs: Array<{ id: string; name: string }>; + constructor(props: AppProps) { + super(props); + this.tabs = [ + { + id: 'dashboardEmbeddable', + name: 'Dashboard Container', + }, + { + id: 'customContainer', + name: 'Custom Container', + }, + { + id: 'visualizeEmbeddable', + name: 'Visualize Embeddable', + }, + { + id: 'helloWorldEmbeddable', + name: 'Hello World Embeddable', + }, + ]; + + this.state = { + selectedTabId: 'dashboardEmbeddable', + }; + } + + public onSelectedTabChanged = (id: string) => { + this.setState({ + selectedTabId: id, + }); + }; + + public renderTabs() { + return this.tabs.map((tab: { id: string; name: string }, index: number) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.state.selectedTabId} + key={index} + > + {tab.name} + + )); + } + + public render() { + return ( +
+
{this.renderTabs()}
+ {this.getContentsForTab()} +
+ ); + } + + private getContentsForTab() { + switch (this.state.selectedTabId) { + case 'dashboardEmbeddable': { + return ; + } + case 'customContainer': { + return ; + } + case 'visualizeEmbeddable': { + return ; + } + case 'helloWorldEmbeddable': { + return ; + } + } + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx new file mode 100644 index 0000000000000..f425ac5ccb1ee --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { EuiButton } from '@elastic/eui'; +import { + DASHBOARD_CONTAINER_TYPE, + DashboardContainer, + DashboardContainerFactory, +} from 'plugins/dashboard_embeddable/index'; +import { ErrorEmbeddable, ViewMode, EmbeddableFactoryRegistry } from 'plugins/embeddable_api/index'; +import React from 'react'; +import { dashboardInput } from './dashboard_input'; + +export interface Props { + embeddableFactories: EmbeddableFactoryRegistry; +} + +export function isErrorEmbeddable( + embeddable: ErrorEmbeddable | DashboardContainer +): embeddable is ErrorEmbeddable { + return (embeddable as ErrorEmbeddable).getInput().errorMessage !== undefined; +} + +export class DashboardContainerExample extends React.Component { + private mounted = false; + private dashboardEmbeddableRoot: React.RefObject; + private container: DashboardContainer | ErrorEmbeddable | undefined; + + public constructor(props: Props) { + super(props); + this.state = { + viewMode: 'view', + }; + + this.dashboardEmbeddableRoot = React.createRef(); + } + + public async componentDidMount() { + this.mounted = true; + const dashboardFactory = this.props.embeddableFactories.getFactoryByName( + DASHBOARD_CONTAINER_TYPE + ) as DashboardContainerFactory; + if (dashboardFactory) { + this.container = await dashboardFactory.create(dashboardInput); + if (this.mounted && this.container && this.dashboardEmbeddableRoot.current) { + this.container.renderInPanel(this.dashboardEmbeddableRoot.current); + } + } + } + + public componentWillUnmount() { + this.mounted = false; + if (this.container) { + this.container.destroy(); + } + } + + public switchViewMode = () => { + this.setState(prevState => { + if (!this.container || isErrorEmbeddable(this.container)) { + return; + } + const newMode = prevState.viewMode === 'view' ? ViewMode.EDIT : ViewMode.VIEW; + this.container.updateInput({ viewMode: newMode }); + return { viewMode: newMode }; + }); + }; + + public render() { + return ( +
+

Embeddable Exploration. Here is a Dashboard Embeddable Container:

+

+ This is a dashboard container rendered using the Embeddable API. It is passed a static + time range. Available actions to take on the dashboard are not being rendered because it's + up to the container to decide how to render them. +

+

+ This dashboard is not tied to a dashboard saved object, but it is tied to visualization + saved objects, so they must exist for this to render properly. +

+ + {this.state.viewMode === 'view' ? 'Edit' : 'View'} + +
+
+ ); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts new file mode 100644 index 0000000000000..01ec099e8cb3c --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts @@ -0,0 +1,224 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DashboardContainerInput } from 'plugins/dashboard_embeddable/index'; +import { QueryLanguageType, ViewMode } from 'plugins/embeddable_api/types'; + +// This output was generated by loading up kibana via: +// node scripts/functional_tests_server --config test/plugin_functional/config +// Then navigating to `Dashboard with everything` dashboard, putting a breakpoint +// where I could grab the DashboardContainer input, copying it into a global +// variable, then JSON.stringify(temp1) it. + +export const dashboardInput: DashboardContainerInput = { + id: 'd2525040-3dcd-11e8-8660-4d65aa086b3c', + filters: [], + hidePanelTitles: false, + query: { language: QueryLanguageType.LUCENE, query: '' }, + timeRange: { from: 'Mon Apr 09 2018 17:56:08 GMT-0400', to: 'Wed Apr 11 2018 17:56:08 GMT-0400' }, + refreshConfig: { value: 0, pause: true }, + viewMode: ViewMode.VIEW, + panels: { + '1': { + type: 'visualization', + gridData: { h: 15, i: '1', w: 24, x: 0, y: 0 }, + embeddableId: '1', + explicitInput: { savedObjectId: 'e6140540-3dca-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '2': { + type: 'visualization', + gridData: { h: 15, i: '2', w: 24, x: 24, y: 0 }, + embeddableId: '2', + explicitInput: { savedObjectId: '3525b840-3dcb-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '3': { + type: 'visualization', + gridData: { h: 15, i: '3', w: 24, x: 0, y: 15 }, + embeddableId: '3', + explicitInput: { savedObjectId: '4b5d6ef0-3dcb-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '4': { + type: 'visualization', + gridData: { h: 15, i: '4', w: 24, x: 24, y: 15 }, + embeddableId: '4', + explicitInput: { savedObjectId: '37a541c0-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '5': { + type: 'visualization', + gridData: { h: 15, i: '5', w: 24, x: 0, y: 30 }, + embeddableId: '5', + explicitInput: { savedObjectId: 'ffa2e0c0-3dcb-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '6': { + type: 'visualization', + gridData: { h: 15, i: '6', w: 24, x: 24, y: 30 }, + embeddableId: '6', + explicitInput: { savedObjectId: 'e2023110-3dcb-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '7': { + type: 'visualization', + gridData: { h: 15, i: '7', w: 24, x: 0, y: 45 }, + embeddableId: '7', + explicitInput: { savedObjectId: '145ced90-3dcb-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '8': { + type: 'visualization', + gridData: { h: 15, i: '8', w: 24, x: 24, y: 45 }, + embeddableId: '8', + explicitInput: { savedObjectId: '2d1b1620-3dcd-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '9': { + type: 'visualization', + gridData: { h: 15, i: '9', w: 24, x: 0, y: 60 }, + embeddableId: '9', + explicitInput: { savedObjectId: '42535e30-3dcd-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '10': { + type: 'visualization', + gridData: { h: 15, i: '10', w: 24, x: 24, y: 60 }, + embeddableId: '10', + explicitInput: { savedObjectId: '42535e30-3dcd-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '11': { + type: 'visualization', + gridData: { h: 15, i: '11', w: 24, x: 0, y: 75 }, + embeddableId: '11', + explicitInput: { savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '12': { + type: 'visualization', + gridData: { h: 15, i: '12', w: 24, x: 24, y: 75 }, + embeddableId: '12', + explicitInput: { savedObjectId: '11ae2bd0-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '13': { + type: 'visualization', + gridData: { h: 15, i: '13', w: 24, x: 0, y: 90 }, + embeddableId: '13', + explicitInput: { savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '14': { + type: 'visualization', + gridData: { h: 15, i: '14', w: 24, x: 24, y: 90 }, + embeddableId: '14', + explicitInput: { savedObjectId: '4ca00ba0-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '15': { + type: 'visualization', + gridData: { h: 15, i: '15', w: 24, x: 0, y: 105 }, + embeddableId: '15', + explicitInput: { savedObjectId: '78803be0-3dcd-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '16': { + type: 'visualization', + gridData: { h: 15, i: '16', w: 24, x: 24, y: 105 }, + embeddableId: '16', + explicitInput: { savedObjectId: 'b92ae920-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '17': { + type: 'visualization', + gridData: { h: 15, i: '17', w: 24, x: 0, y: 120 }, + embeddableId: '17', + explicitInput: { savedObjectId: 'e4d8b430-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '18': { + type: 'visualization', + gridData: { h: 15, i: '18', w: 24, x: 24, y: 120 }, + embeddableId: '18', + explicitInput: { savedObjectId: 'f81134a0-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '19': { + type: 'visualization', + gridData: { h: 15, i: '19', w: 24, x: 0, y: 135 }, + embeddableId: '19', + explicitInput: { savedObjectId: 'cc43fab0-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '20': { + type: 'visualization', + gridData: { h: 15, i: '20', w: 24, x: 24, y: 135 }, + embeddableId: '20', + explicitInput: { savedObjectId: '02a2e4e0-3dcd-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '21': { + type: 'visualization', + gridData: { h: 15, i: '21', w: 24, x: 0, y: 150 }, + embeddableId: '21', + explicitInput: { savedObjectId: 'df815d20-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '22': { + type: 'visualization', + gridData: { h: 15, i: '22', w: 24, x: 24, y: 150 }, + embeddableId: '22', + explicitInput: { savedObjectId: 'c40f4d40-3dcc-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '23': { + type: 'visualization', + gridData: { h: 15, i: '23', w: 24, x: 0, y: 165 }, + embeddableId: '23', + explicitInput: { savedObjectId: '7fda8ee0-3dcd-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '24': { + type: 'search', + gridData: { h: 15, i: '24', w: 24, x: 24, y: 165 }, + embeddableId: '24', + explicitInput: { savedObjectId: 'a16d1990-3dca-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '25': { + type: 'search', + gridData: { h: 15, i: '25', w: 24, x: 0, y: 180 }, + embeddableId: '25', + explicitInput: { savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '26': { + type: 'search', + gridData: { h: 15, i: '26', w: 24, x: 24, y: 180 }, + embeddableId: '26', + explicitInput: { savedObjectId: 'ca5ada40-3dca-11e8-8660-4d65aa086b3c', customization: {} }, + }, + '27': { + type: 'visualization', + gridData: { h: 15, i: '27', w: 24, x: 0, y: 195 }, + embeddableId: '27', + explicitInput: { savedObjectId: '771b4f10-3e59-11e8-9fc3-39e49624228e', customization: {} }, + }, + '28': { + type: 'visualization', + gridData: { h: 15, i: '28', w: 24, x: 24, y: 195 }, + embeddableId: '28', + explicitInput: { savedObjectId: '5e085850-3e6e-11e8-bbb9-e15942d5d48c', customization: {} }, + }, + '29': { + type: 'visualization', + gridData: { h: 15, i: '29', w: 24, x: 0, y: 210 }, + embeddableId: '29', + explicitInput: { savedObjectId: '8bc8d6c0-3e6e-11e8-bbb9-e15942d5d48c', customization: {} }, + }, + '30': { + type: 'visualization', + gridData: { h: 15, i: '30', w: 24, x: 24, y: 210 }, + embeddableId: '30', + explicitInput: { savedObjectId: 'befdb6b0-3e59-11e8-9fc3-39e49624228e', customization: {} }, + }, + }, + isFullScreenMode: false, + useMargins: true, + lastReloadRequestTime: 1555421511389, + title: 'dashboard with everything', + description: 'I have one of every visualization type since the last time I was created!', +}; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_embeddable_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_embeddable_example.tsx new file mode 100644 index 0000000000000..063bde0c54a39 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_embeddable_example.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { HelloWorldEmbeddable } from '../embeddables/hello_world_embeddable'; + +export class HelloWorldEmbeddableExample extends React.Component<{}> { + private embeddableRoot: React.RefObject; + private embeddable?: HelloWorldEmbeddable; + + public constructor() { + super({}); + this.embeddableRoot = React.createRef(); + } + + public async componentDidMount() { + if (this.embeddableRoot.current) { + this.embeddable = new HelloWorldEmbeddable({ id: 'hello' }); + this.embeddable.renderInPanel(this.embeddableRoot.current); + } + } + + public componentWillUnmount() { + if (this.embeddable) { + this.embeddable.destroy(); + } + } + + public render() { + return ( +
+
+
+ ); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/index.ts new file mode 100644 index 0000000000000..89b236b3bd52a --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { App } from './app'; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/list_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/list_container_example.tsx new file mode 100644 index 0000000000000..3c619c8159fb8 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/list_container_example.tsx @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + Container, + embeddableFactories, + EmbeddableFactoryRegistry, +} from 'plugins/embeddable_api/index'; +import React from 'react'; +import { ListContainer } from '../embeddables/list_container'; + +interface Props { + embeddableFactories: EmbeddableFactoryRegistry; +} + +export class ListContainerExample extends React.Component { + private root: React.RefObject; + private container: Container; + + public constructor(props: Props) { + super(props); + + this.root = React.createRef(); + this.container = new ListContainer(embeddableFactories); + } + + public async componentDidMount() { + if (this.root.current) { + this.container.renderInPanel(this.root.current); + } + } + + public componentWillUnmount() { + if (this.container) { + this.container.destroy(); + } + } + public render() { + return ( +
+

Custom Embeddable Container:

+

+ This is a custom container object, to show that visualize embeddables can be rendered + outside of the dashboard container. +

+
+
+ ); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/visualize_embeddable_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/visualize_embeddable_example.tsx new file mode 100644 index 0000000000000..3f2ff1d57a1e6 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/visualize_embeddable_example.tsx @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { EmbeddableFactoryRegistry } from 'plugins/embeddable_api/index'; +import { + DisabledLabEmbeddable, + VISUALIZE_EMBEDDABLE_TYPE, + VisualizeEmbeddable, + VisualizeEmbeddableFactory, + VisualizeInput, +} from 'plugins/kibana/visualize/embeddable'; +import React from 'react'; + +export interface Props { + embeddableFactories: EmbeddableFactoryRegistry; +} + +export class VisualizeEmbeddableExample extends React.Component { + private mounted = false; + private embeddableRoot: React.RefObject; + private embeddable: VisualizeEmbeddable | DisabledLabEmbeddable | undefined; + + public constructor(props: Props) { + super(props); + this.state = { + viewMode: 'view', + }; + + this.embeddableRoot = React.createRef(); + } + + public async componentDidMount() { + this.mounted = true; + const visualizeFactory = this.props.embeddableFactories.getFactoryByName( + VISUALIZE_EMBEDDABLE_TYPE + ) as VisualizeEmbeddableFactory; + if (visualizeFactory) { + const input: VisualizeInput = { + customization: {}, + }; + this.embeddable = await visualizeFactory.create( + { + id: 'ed8436b0-b88b-11e8-a6d9-e546fe2bba5f', + }, + input + ); + if (this.mounted && this.embeddable && this.embeddableRoot.current) { + this.embeddable.render(this.embeddableRoot.current); + } + } + } + + public componentWillUnmount() { + this.mounted = false; + if (this.embeddable) { + this.embeddable.destroy(); + } + } + + public render() { + return ( +
+
+
+ ); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/button_embeddable.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/button_embeddable.tsx new file mode 100644 index 0000000000000..710d96d627d0b --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/button_embeddable.tsx @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// /* +// * Licensed to Elasticsearch B.V. under one or more contributor +// * license agreements. See the NOTICE file distributed with +// * this work for additional information regarding copyright +// * ownership. Elasticsearch B.V. licenses this file to you under +// * the Apache License, Version 2.0 (the "License"); you may +// * not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, +// * software distributed under the License is distributed on an +// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// * KIND, either express or implied. See the License for the +// * specific language governing permissions and limitations +// * under the License. +// */ + +// import { Embeddable, triggerRegistry } from 'plugins/embeddable_api/index'; +// import React from 'react'; +// import ReactDom from 'react-dom'; +// import { kfetch } from 'ui/kfetch'; +// import { +// BUTTON_EMBEDDABLE, +// ButtonEmbeddableInput, +// ButtonEmbeddableOutput, +// } from './button_embeddable_factory'; +// import { ButtonCard } from './user_card'; + +// export const BUTTON_CLICK_TRIGGER = 'BUTTON_CLICK_TRIGGER'; + +// export class ButtonEmbeddable extends Embeddable { +// private unsubscribe: () => void; +// constructor(initialInput: ButtonEmbeddableInput) { +// super(BUTTON_EMBEDDABLE, initialInput, {}); +// this.unsubscribe = this.subscribeToInputChanges(() => this.initializeOutput()); +// this.initializeOutput(); +// } + +// public destroy() { +// this.unsubscribe(); +// } + +// public render(node: HTMLElement) { +// ReactDom.render(, node); +// } + +// private async initializeOutput() { +// // const usersUrl = chrome.addBasePath('/api/security/v1/users'); +// try { +// const usersUrl = '/api/security/v1/users'; +// const url = `${usersUrl}/${this.input.username}`; +// const { data } = await kfetch({ pathname: url }); +// this.emitOutputChanged({ +// user: { +// ...data, +// }, +// }); +// } catch (e) { +// this.output.error = e; +// } +// } +// } + +// triggerRegistry.registerTrigger({ +// id: VIEW_USER_TRIGGER, +// title: 'View user', +// }); + +// triggerRegistry.registerTrigger({ +// id: EMAIL_USER_TRIGGER, +// title: 'Email user', +// }); + +// // triggerRegistry.addDefaultAction({ triggerId: VIEW_EMPLOYEE_TRIGGER, actionId: }) diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/button_embeddable_factory.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/button_embeddable_factory.ts new file mode 100644 index 0000000000000..305908748cc9a --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/button_embeddable_factory.ts @@ -0,0 +1,64 @@ +// /* +// * Licensed to Elasticsearch B.V. under one or more contributor +// * license agreements. See the NOTICE file distributed with +// * this work for additional information regarding copyright +// * ownership. Elasticsearch B.V. licenses this file to you under +// * the Apache License, Version 2.0 (the "License"); you may +// * not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, +// * software distributed under the License is distributed on an +// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// * KIND, either express or implied. See the License for the +// * specific language governing permissions and limitations +// * under the License. +// */ + +// import 'ui/doc_table'; + +// import { +// embeddableFactories, +// EmbeddableFactory, +// EmbeddableInput, +// EmbeddableOutput, +// } from 'plugins/embeddable_api/index'; +// import chrome from 'ui/chrome'; +// import { openFlyout } from 'ui/flyout'; +// import { User } from '../users_embeddable/users_embeddable_factory'; +// import { UserEmbeddable } from './button_embeddable'; + +// export const USER_EMBEDDABLE = 'USER_EMBEDDABLE'; + +// export interface UserEmbeddableInput extends EmbeddableInput { +// username: string; +// } + +// export interface UserEmbeddableOutput extends EmbeddableOutput { +// user?: User; +// error?: string; +// } + +// export class UserEmbeddableFactory extends EmbeddableFactory< +// UserEmbeddableInput, +// UserEmbeddableOutput, +// { username: string; email: string; full_name: string } +// > { +// constructor() { +// super({ +// name: USER_EMBEDDABLE, +// }); +// } + +// public getOutputSpec() { +// return {}; +// } + +// public async create(initialInput: UserEmbeddableInput) { +// return Promise.resolve(new UserEmbeddable(initialInput)); +// } +// } + +// embeddableFactories.registerFactory(new UserEmbeddableFactory()); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/user_card.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/user_card.tsx new file mode 100644 index 0000000000000..e87c944ec50fc --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/button_embeddable/user_card.tsx @@ -0,0 +1,173 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// /* +// * Licensed to Elasticsearch B.V. under one or more contributor +// * license agreements. See the NOTICE file distributed with +// * this work for additional information regarding copyright +// * ownership. Elasticsearch B.V. licenses this file to you under +// * the Apache License, Version 2.0 (the "License"); you may +// * not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, +// * software distributed under the License is distributed on an +// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// * KIND, either express or implied. See the License for the +// * specific language governing permissions and limitations +// * under the License. +// */ +// // @ts-ignore +// import { +// EuiButton, +// EuiCallOut, +// EuiCard, +// EuiFieldText, +// EuiFlexItem, +// EuiFormRow, +// EuiLink, +// EuiSpacer, +// EuiText, +// } from '@elastic/eui'; +// import { executeTriggerActions, getActionsForTrigger } from 'plugins/embeddable_api/index'; +// import React from 'react'; +// import { User } from '../users_embeddable/users_embeddable_factory'; +// import { EMAIL_USER_TRIGGER, UserEmbeddable, VIEW_USER_TRIGGER } from './button_embeddable'; + +// interface Props { +// embeddable: UserEmbeddable; +// } + +// interface State { +// user?: User; +// error?: string; +// viewActionIsConfigured: boolean; +// emailActionIsConfigured: boolean; +// username?: string; +// } + +// export class UserCard extends React.Component { +// private mounted = false; +// private unsubscribe?: () => void; + +// constructor(props: Props) { +// super(props); +// const { user, error } = props.embeddable.getOutput(); +// this.state = { +// user, +// username: props.embeddable.getInput().username, +// error, +// viewActionIsConfigured: false, +// emailActionIsConfigured: false, +// }; +// } + +// public renderCardFooterContent() { +// return ( +//
+// this.onView()}>View +// +// +//

+// this.onEmail()}>Email +//

+//
+//
+// ); +// } + +// public async componentDidMount() { +// this.mounted = true; +// this.unsubscribe = this.props.embeddable.subscribeToOutputChanges(() => { +// if (this.mounted) { +// const { user, error } = this.props.embeddable.getOutput(); +// this.setState({ user, error }); +// } +// }); + +// const viewActions = await getActionsForTrigger(VIEW_USER_TRIGGER, { +// embeddable: this.props.embeddable, +// }); + +// const emailActions = await getActionsForTrigger(EMAIL_USER_TRIGGER, { +// embeddable: this.props.embeddable, +// }); + +// if (this.mounted) { +// this.setState({ +// emailActionIsConfigured: emailActions.length > 0, +// viewActionIsConfigured: viewActions.length > 0, +// }); +// } +// } + +// public componentWillUnmount() { +// if (this.unsubscribe) { +// this.unsubscribe(); +// } +// this.mounted = false; +// } + +// public changeUser = () => { +// if (this.state.username) { +// this.props.embeddable.setInput({ +// ...this.props.embeddable.getInput(), +// username: this.state.username, +// }); +// } +// }; + +// public render() { +// return ( +// +// {this.state.user ? ( +// +// ) : ( +// +// Error:{this.state.error ? this.state.error.message : ''} +// +// this.setState({ username: e.target.value })} /> +// +// Change user +// +// )} +// +// ); +// } + +// private onView = () => { +// executeTriggerActions(VIEW_USER_TRIGGER, { +// embeddable: this.props.embeddable, +// triggerContext: {}, +// }); +// }; + +// private onEmail = () => { +// executeTriggerActions(EMAIL_USER_TRIGGER, { +// embeddable: this.props.embeddable, +// triggerContext: {}, +// }); +// }; +// } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/hello_world_embeddable.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/hello_world_embeddable.tsx new file mode 100644 index 0000000000000..6a412f07253cc --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/hello_world_embeddable.tsx @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Embeddable, EmbeddableInput } from 'plugins/embeddable_api/index'; +import React from 'react'; +import ReactDom from 'react-dom'; +import { HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable_factory'; + +export class HelloWorldEmbeddable extends Embeddable { + constructor(initialInput: EmbeddableInput) { + super(HELLO_WORLD_EMBEDDABLE, initialInput, { title: 'Hello World!' }); + } + + public render(node: HTMLElement) { + ReactDom.render(
Hello World!
, node); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/hello_world_embeddable_factory.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/hello_world_embeddable_factory.ts new file mode 100644 index 0000000000000..df51121e3209c --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/hello_world_embeddable_factory.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + embeddableFactories, + EmbeddableFactory, + EmbeddableInput, +} from 'plugins/embeddable_api/index'; +import { HelloWorldEmbeddable } from './hello_world_embeddable'; + +export const HELLO_WORLD_EMBEDDABLE = 'hello_world'; + +export class HelloWorldEmbeddableFactory extends EmbeddableFactory { + constructor() { + super({ + name: HELLO_WORLD_EMBEDDABLE, + }); + } + + public getOutputSpec() { + return {}; + } + + public create(initialInput: EmbeddableInput) { + return Promise.resolve(new HelloWorldEmbeddable(initialInput)); + } +} + +embeddableFactories.registerFactory(new HelloWorldEmbeddableFactory()); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/list_container.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/list_container.tsx new file mode 100644 index 0000000000000..5eec89cf108df --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/list_container.tsx @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Container, EmbeddableFactoryRegistry } from 'plugins/embeddable_api/index'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { ListDisplay } from './list_display'; + +export const LIST_CONTAINER_ID = 'LIST_CONTAINER_ID'; + +export class ListContainer extends Container { + constructor(embeddableFactories: EmbeddableFactoryRegistry) { + // Seed the list with one embeddable to ensure it works. + super( + LIST_CONTAINER_ID, + { + id: '1234', + panels: { + myid: { + initialInput: {}, + customization: {}, + embeddableId: 'myid', + type: 'hello_world', + }, + }, + }, + { embeddableLoaded: { myid: false } }, + embeddableFactories + ); + } + + public render(node: HTMLElement) { + ReactDOM.render(, node); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/list_display.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/list_display.tsx new file mode 100644 index 0000000000000..8be77f041a50b --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/embeddables/list_display.tsx @@ -0,0 +1,150 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Container, Embeddable } from 'plugins/embeddable_api/index'; +import React, { Component, Ref, RefObject } from 'react'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface Props { + container: Container; +} + +interface State { + embeddables: { [key: string]: Embeddable }; + loaded: { [key: string]: boolean }; +} + +export class ListDisplay extends Component { + private roots: { [key: string]: RefObject } = {}; + private mounted: boolean = false; + private inputSubscription?: Subscription; + private outputSubscription?: Subscription; + + public constructor(props: Props) { + super(props); + + Object.values(this.props.container.getInput().panels).forEach(panelState => { + this.roots[panelState.embeddableId] = React.createRef(); + }); + this.state = { + loaded: {}, + embeddables: {}, + }; + } + + public async componentDidMount() { + this.mounted = true; + + Object.values(this.props.container.getInput().panels).forEach(panelState => { + this.renderEmbeddable(panelState.embeddableId); + }); + + this.inputSubscription = this.props.container.getInput$().subscribe(input => { + Object.values(input.panels).forEach(async panelState => { + if (this.roots[panelState.embeddableId] === undefined) { + this.roots[panelState.embeddableId] = React.createRef(); + } + + if (this.state.embeddables[panelState.embeddableId] === undefined) { + const embeddable = await this.props.container.getEmbeddable(panelState.embeddableId); + const node = this.roots[panelState.embeddableId].current; + if (this.mounted && node !== null && embeddable) { + embeddable.renderInPanel(node, this.props.container); + + this.setState(prevState => ({ + loaded: { ...prevState.loaded, [panelState.embeddableId]: true }, + })); + } + } + }); + }); + + this.outputSubscription = this.props.container.getOutput$().subscribe(output => { + const embeddablesLoaded = output.embeddableLoaded; + Object.keys(embeddablesLoaded).forEach(async id => { + const loaded = embeddablesLoaded[id]; + if (loaded && !this.state.loaded[id]) { + const embeddable = await this.props.container.getEmbeddable(id); + const node = this.roots[id].current; + if (this.mounted && node !== null && embeddable) { + embeddable.renderInPanel(node); + this.setState({ loaded: embeddablesLoaded }); + } + } + }); + }); + } + + public componentWillUnmount() { + this.props.container.destroy(); + + if (this.inputSubscription) { + this.inputSubscription.unsubscribe(); + } + if (this.outputSubscription) { + this.outputSubscription.unsubscribe(); + } + } + + public renderList() { + const list = Object.values(this.props.container.getInput().panels).map(panelState => { + const item = ( + +
+ + ); + return item; + }); + return list; + } + + public render() { + return ( +
+

A list of Embeddables!

+ {this.renderList()} +
+ ); + } + + private async renderEmbeddable(id: string) { + if (this.state.embeddables[id] !== undefined) { + return; + } + + if (this.roots[id] === undefined) { + this.roots[id] = React.createRef(); + } + + if (this.state.embeddables[id] === undefined) { + const embeddable = await this.props.container.getEmbeddable(id); + const node = this.roots[id].current; + if (this.mounted && node !== null && embeddable) { + embeddable.renderInPanel(node); + + this.setState(prevState => ({ + loaded: { ...prevState.loaded, [id]: true }, + embeddables: { + [id]: embeddable, + }, + })); + } + } + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.html b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.html new file mode 100644 index 0000000000000..a242631e1638f --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.html @@ -0,0 +1,3 @@ + +
ANGULAR STUFF!
+ diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.ts new file mode 100644 index 0000000000000..0f1dc73545d24 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin as EmbeddableExplorer } from './plugin'; +import { createShim } from './shim'; +const embeddableExplorer = new EmbeddableExplorer(); +embeddableExplorer.start(createShim()); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx new file mode 100644 index 0000000000000..73a8baab9dbfe --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/plugin.tsx @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { App } from './app/'; +import { CoreShim, PluginShim } from './shim'; + +const REACT_ROOT_ID = 'embeddableExplorerRoot'; + +export class Plugin { + public start({ core, plugins }: { core: CoreShim; plugins: PluginShim }): void { + core.onRenderComplete(() => { + const root = document.getElementById(REACT_ROOT_ID); + ReactDOM.render( + , + root + ); + }); + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx new file mode 100644 index 0000000000000..14ef76f9db598 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { embeddableFactories, EmbeddableFactoryRegistry } from 'plugins/embeddable_api/index'; + +import 'ui/autoload/all'; +import 'uiExports/embeddableActions'; +import 'uiExports/embeddableFactories'; + +import uiRoutes from 'ui/routes'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import template from './index.html'; + +export interface PluginShim { + embeddableAPI: { + embeddableFactories: EmbeddableFactoryRegistry; + }; +} + +export interface CoreShim { + onRenderComplete: (listener: () => void) => void; +} + +const pluginShim: PluginShim = { + embeddableAPI: { + embeddableFactories, + }, +}; + +let rendered = false; +const onRenderCompleteListeners: Array<() => void> = []; +const coreShim: CoreShim = { + onRenderComplete: (renderCompleteListener: () => void) => { + if (rendered) { + renderCompleteListener(); + } else { + onRenderCompleteListeners.push(renderCompleteListener); + } + }, +}; + +uiRoutes.enable(); +uiRoutes.defaults(/\embeddable_explorer/, {}); +uiRoutes.when('/', { + template, + controller($scope) { + $scope.$$postDigest(() => { + rendered = true; + onRenderCompleteListeners.forEach(listener => listener()); + }); + }, +}); + +export function createShim(): { core: CoreShim; plugins: PluginShim } { + return { + core: coreShim, + plugins: pluginShim, + }; +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/shim.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/shim.ts new file mode 100644 index 0000000000000..faf66d62b17e8 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/shim.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CoreShim } from '.'; + +export function createShim(): { core: CoreShim } { + return { + core: {}, + }; +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json new file mode 100644 index 0000000000000..aba7203377aff --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx index 3f04adc11a418..23247e6046998 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx @@ -23,8 +23,8 @@ import { getNewPlatform } from 'ui/new_platform'; import { ContextMenuAction, ContextMenuActionsRegistryProvider, - PanelActionAPI, -} from 'ui/embeddable'; + ExecuteActionContext, +} from 'plugins/embeddable_api/index'; class SamplePanelAction extends ContextMenuAction { constructor() { @@ -34,7 +34,7 @@ class SamplePanelAction extends ContextMenuAction { parentPanelId: 'mainMenu', }); } - public onClick = ({ embeddable }: PanelActionAPI) => { + public onClick = ({ embeddable }: ExecuteActionContext) => { if (!embeddable) { return; } diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts index 7092c783125d9..7ebf547647fe5 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable'; +import { + ContextMenuAction, + ContextMenuActionsRegistryProvider, +} from 'plugins/embeddable_api/index'; class SamplePanelLink extends ContextMenuAction { constructor() { diff --git a/test/plugin_functional/test_suites/embeddable_explorer/embeddables_render.js b/test/plugin_functional/test_suites/embeddable_explorer/embeddables_render.js new file mode 100644 index 0000000000000..1acc46e1298bd --- /dev/null +++ b/test/plugin_functional/test_suites/embeddable_explorer/embeddables_render.js @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService }) { + const find = getService('find'); + const pieChart = getService('pieChart'); + const dashboardExpect = getService('dashboardExpect'); + + describe('renders', () => { + it('pie charts', async () => { + await pieChart.expectPieSliceCount(8); + }); + + it('metric visualizations', async () => { + await dashboardExpect.metricValuesExist(['5,922']); + }); + + it('visualizations with series and line charts', async () => { + await dashboardExpect.seriesElementCount(30); + await dashboardExpect.lineChartPointsCount(5); + }); + + it('tsvb visualizations', async () => { + const tsvbGuageExists = await find.existsByCssSelector('.tvbVisHalfGauge'); + expect(tsvbGuageExists).to.be(true); + await dashboardExpect.tsvbTimeSeriesLegendCount(1); + await dashboardExpect.tsvbTableCellCount(20); + await dashboardExpect.tsvbMarkdownWithValuesExists(['Hi Avg last bytes: 5919.43661971831']); + await dashboardExpect.tsvbTopNValuesExist(['5,544.25', '5,919.437']); + await dashboardExpect.tsvbMetricValuesExist(['3,526,809,089']); + }); + + it('markdown', async () => { + await dashboardExpect.markdownWithValuesExists(['I\'m a markdown!']); + }); + + it('goal and guage', async () => { + await dashboardExpect.goalAndGuageLabelsExist(['40%', '37%', '11.944 GB']); + }); + + it('data table', async () => { + await dashboardExpect.dataTableRowCount(5); + }); + + it('tag cloud', async () => { + await dashboardExpect.tagCloudWithValuesFound(['CN', 'IN', 'US', 'BR', 'ID']); + }); + + it('input controls', async () => { + await dashboardExpect.inputControlItemCount(7); + }); + + it('saved search', async () => { + await dashboardExpect.savedSearchRowCount(55); + }); + }); +} diff --git a/test/plugin_functional/test_suites/embeddable_explorer/index.js b/test/plugin_functional/test_suites/embeddable_explorer/index.js new file mode 100644 index 0000000000000..ad25167f9fc91 --- /dev/null +++ b/test/plugin_functional/test_suites/embeddable_explorer/index.js @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function ({ getService, getPageObjects, loadTestFile }) { + const browser = getService('browser'); + const appsMenu = getService('appsMenu'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('embeddable explorer', function () { + before(async () => { + await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data'); + await esArchiver.load('../functional/fixtures/es_archiver/dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', 'defaultIndex': 'logstash-*' }); + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('settings'); + console.log('clicking embed'); + await appsMenu.clickLink('Embeddable Explorer'); + }); + + loadTestFile(require.resolve('./embeddables_render')); + }); +}