From aa80766f02222b2f2511aa66e80e3fa2859f59ad Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 25 Jun 2019 09:49:34 -0400 Subject: [PATCH] Dashboard embeddable container plugin (#38974) (#39573) * Dashboard embeddable plugin * comment out duplicate scss styles * review: conform closer to NP standards * export/import from index file in folder --- .i18nrc.json | 1 + .../dashboard_embeddable_container/index.ts | 30 ++ .../package.json | 4 + .../actions/expand_panel_action.test.tsx | 103 ++++++ .../public/actions/expand_panel_action.tsx | 96 ++++++ .../public/actions/index.ts | 20 ++ .../public/embeddable/_index.scss | 3 + .../public/embeddable/dashboard_constants.ts | 23 ++ .../embeddable/dashboard_container.test.tsx | 205 ++++++++++++ .../public/embeddable/dashboard_container.tsx | 141 +++++++++ .../embeddable/dashboard_container_factory.ts | 80 +++++ .../embeddable/grid/_dashboard_grid.scss | 127 ++++++++ .../public/embeddable/grid/_index.scss | 3 + .../embeddable/grid/dashboard_grid.test.tsx | 138 ++++++++ .../public/embeddable/grid/dashboard_grid.tsx | 296 ++++++++++++++++++ .../public/embeddable/grid/index.ts | 20 ++ .../public/embeddable/index.ts | 30 ++ .../embeddable/panel/_dashboard_panel.scss | 24 ++ .../public/embeddable/panel/_index.scss | 1 + .../panel/create_panel_state.test.ts | 95 ++++++ .../embeddable/panel/create_panel_state.ts | 119 +++++++ .../public/embeddable/panel/index.ts | 20 ++ .../public/embeddable/types.ts | 34 ++ .../viewport/_dashboard_viewport.scss | 8 + .../public/embeddable/viewport/_index.scss | 1 + .../viewport/dashboard_viewport.test.tsx | 133 ++++++++ .../viewport/dashboard_viewport.tsx | 104 ++++++ .../public/index.scss | 6 + .../public/index.ts | 31 ++ .../public/np_core.test.mocks.ts | 65 ++++ .../public/shim/index.ts | 37 +++ .../public/shim/plugin.ts | 48 +++ .../get_sample_dashboard_input.ts | 64 ++++ .../public/test_helpers/index.ts | 20 ++ .../public/containers/i_container.ts | 6 +- .../public/embeddables/embeddable_factory.ts | 2 +- .../public/embeddables/i_embeddable.ts | 7 +- .../embeddable_api/public/index.ts | 4 +- .../embeddable_api/public/plugin.ts | 30 ++ .../embeddable_api/public/types.ts | 9 + .../kbn_tp_embeddable_explorer/index.ts | 1 + .../public/app/app.tsx | 8 + .../app/dashboard_container_example.tsx | 100 ++++++ .../public/app/dashboard_input.ts | 122 ++++++++ .../dashboard_container.js | 55 ++++ .../test_suites/embeddable_explorer/index.js | 1 + 46 files changed, 2468 insertions(+), 7 deletions(-) create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/index.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/package.json create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/actions/expand_panel_action.test.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/actions/index.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/_index.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_constants.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.test.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_index.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/index.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/index.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_dashboard_panel.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_index.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/create_panel_state.test.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/create_panel_state.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/index.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/types.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_index.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.tsx create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/shim/index.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/shim/plugin.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/get_sample_dashboard_input.ts create mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/index.ts create mode 100644 src/legacy/core_plugins/embeddable_api/public/plugin.ts create mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx create mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts create mode 100644 test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js diff --git a/.i18nrc.json b/.i18nrc.json index d675eb02479a..72a352fabe61 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -8,6 +8,7 @@ "inputControl": "src/legacy/core_plugins/input_control_vis", "inspectorViews": "src/legacy/core_plugins/inspector_views", "interpreter": "src/legacy/core_plugins/interpreter", + "dashboardEmbeddableContainer": "src/legacy/core_plugins/dashboard_embeddable_container", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", "embeddableApi": "src/legacy/core_plugins/embeddable_api", diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/index.ts new file mode 100644 index 000000000000..b2d5bceafdee --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { resolve } from 'path'; + +// eslint-disable-next-line import/no-default-export +export default function(kibana: any) { + return new kibana.Plugin({ + uiExports: { + hacks: 'plugins/dashboard_embeddable_container/shim', + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + }, + }); +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/package.json b/src/legacy/core_plugins/dashboard_embeddable_container/package.json new file mode 100644 index 000000000000..7555895e8d71 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/package.json @@ -0,0 +1,4 @@ +{ + "name": "dashboard_embeddable_container", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/expand_panel_action.test.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/expand_panel_action.test.tsx new file mode 100644 index 000000000000..40e5ee2cb776 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/expand_panel_action.test.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 '../np_core.test.mocks'; + +import { isErrorEmbeddable, EmbeddableFactory } from '../../../embeddable_api/public'; +import { ExpandPanelAction } from './expand_panel_action'; +import { + ContactCardEmbeddable, + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddableFactory, +} from '../../../embeddable_api/public/test_samples/index'; +import { DashboardContainer } from '../embeddable'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers'; + +const embeddableFactories = new Map(); +embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + +let container: DashboardContainer; +let embeddable: ContactCardEmbeddable; + +beforeEach(async () => { + container = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, + }), + embeddableFactories + ); + + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Kibana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } else { + embeddable = contactCardEmbeddable; + } +}); + +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 ContactCardEmbeddable({ 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.getDisplayName({ 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_container/public/actions/expand_panel_action.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx new file mode 100644 index 000000000000..7a0187066534 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx @@ -0,0 +1,96 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + Action, + IEmbeddable, + ActionContext, + IncompatibleActionError, +} from '../../../embeddable_api/public'; +import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; + +export const EXPAND_PANEL_ACTION = 'togglePanel'; + +function isDashboard( + embeddable: IEmbeddable | DashboardContainer +): embeddable is DashboardContainer { + return (embeddable as DashboardContainer).type === DASHBOARD_CONTAINER_TYPE; +} + +function isExpanded(embeddable: IEmbeddable) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + + return embeddable.id === embeddable.parent.getInput().expandedPanelId; +} + +export class ExpandPanelAction extends Action { + public readonly type = EXPAND_PANEL_ACTION; + + constructor() { + super(EXPAND_PANEL_ACTION); + this.order = 7; + } + + public getDisplayName({ embeddable }: ActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + + return isExpanded(embeddable) + ? i18n.translate( + 'dashboardEmbeddableContainer.actions.toggleExpandPanelMenuItem.expandedDisplayName', + { + defaultMessage: 'Minimize', + } + ) + : i18n.translate( + 'dashboardEmbeddableContainer.actions.toggleExpandPanelMenuItem.notExpandedDisplayName', + { + defaultMessage: 'Full screen', + } + ); + } + + public getIcon({ embeddable }: ActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + // TODO: use 'minimize' when an eui-icon of such is available. + return ; + } + + public async isCompatible({ embeddable }: ActionContext) { + return Boolean(embeddable.parent && isDashboard(embeddable.parent)); + } + + public execute({ embeddable }: ActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + const newValue = isExpanded(embeddable) ? undefined : embeddable.id; + embeddable.parent.updateInput({ + expandedPanelId: newValue, + }); + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/index.ts new file mode 100644 index 000000000000..b0707610cf21 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/actions/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 { ExpandPanelAction, EXPAND_PANEL_ACTION } from './expand_panel_action'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/_index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/_index.scss new file mode 100644 index 000000000000..f84767218ebc --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/_index.scss @@ -0,0 +1,3 @@ +@import './viewport/index'; +@import './panel/index'; +@import './grid/index'; \ No newline at end of file diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_constants.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_constants.ts new file mode 100644 index 000000000000..941ddd3c5efe --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_constants.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 const DASHBOARD_GRID_COLUMN_COUNT = 48; +export const DASHBOARD_GRID_HEIGHT = 20; +export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2; +export const DEFAULT_PANEL_HEIGHT = 15; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.test.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.test.tsx new file mode 100644 index 000000000000..69130168f26e --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.test.tsx @@ -0,0 +1,205 @@ +/* + * 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 '../np_core.test.mocks'; + +import React from 'react'; + +import { + isErrorEmbeddable, + ViewMode, + actionRegistry, + triggerRegistry, + CONTEXT_MENU_TRIGGER, + attachAction, + EmbeddableFactory, +} from '../../../embeddable_api/public'; +import { DashboardContainer } from './dashboard_container'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers'; +import { mount } from 'enzyme'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddablePanel } from '../../../embeddable_api/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { + ContactCardEmbeddableOutput, + EditModeAction, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, +} from '../../../embeddable_api/public/test_samples'; + +test('DashboardContainer initializes embeddables', async done => { + const embeddableFactories = new Map(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + const container = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_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 Map(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + const container = new DashboardContainer(getSampleDashboardInput(), embeddableFactories); + const embeddable = await container.addNewEmbeddable( + CONTACT_CARD_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 existing children', async () => { + const embeddableFactories = new Map(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + const container = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, + }), + embeddableFactories + ); + await nextTick(); + + const embeddable = await container.getChild('123'); + expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW); + container.updateInput({ viewMode: ViewMode.EDIT }); + expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); +}); + +test('Container view mode change propagates to new children', async () => { + const embeddableFactories = new Map(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + const container = new DashboardContainer(getSampleDashboardInput(), embeddableFactories); + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Bob', + }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW); + + container.updateInput({ viewMode: ViewMode.EDIT }); + + expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); +}); + +test('DashboardContainer in edit mode shows edit mode actions', async () => { + const editModeAction = new EditModeAction(); + actionRegistry.set(editModeAction.id, editModeAction); + attachAction(triggerRegistry, { + triggerId: CONTEXT_MENU_TRIGGER, + actionId: editModeAction.id, + }); + + const embeddableFactories = new Map(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + const container = new DashboardContainer( + getSampleDashboardInput({ viewMode: ViewMode.VIEW }), + embeddableFactories + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_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(); + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + 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_container/public/embeddable/dashboard_container.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx new file mode 100644 index 000000000000..e4dbb2e7bd12 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx @@ -0,0 +1,141 @@ +/* + * 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 { I18nProvider } from '@kbn/i18n/react'; +import { IndexPattern } from 'ui/index_patterns'; + +import { Filter } from '@kbn/es-query'; +import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { + Container, + ContainerInput, + EmbeddableInput, + ViewMode, + isErrorEmbeddable, + EmbeddableFactory, + IEmbeddable, +} from '../../../embeddable_api/public/index'; + +import { DASHBOARD_CONTAINER_TYPE } from './dashboard_container_factory'; +import { createPanelState } from './panel'; +import { DashboardPanelState } from './types'; +import { DashboardViewport } from './viewport/dashboard_viewport'; +import { Query } from '../../../data/public'; + +export interface DashboardContainerInput extends ContainerInput { + viewMode: ViewMode; + filters: Filter[]; + query: Query; + timeRange: TimeRange; + refreshConfig?: RefreshInterval; + expandedPanelId?: string; + useMargins: boolean; + title: string; + description?: string; + isFullScreenMode: boolean; + panels: { [panelId: string]: DashboardPanelState }; +} + +interface IndexSignature { + [key: string]: unknown; +} + +export interface InheritedChildInput extends IndexSignature { + filters: Filter[]; + query: Query; + timeRange: TimeRange; + refreshConfig?: RefreshInterval; + viewMode: ViewMode; + hidePanelTitles?: boolean; + id: string; +} + +export class DashboardContainer extends Container { + public readonly type = DASHBOARD_CONTAINER_TYPE; + + constructor( + initialInput: DashboardContainerInput, + embeddableFactories: Map, + parent?: Container + ) { + super( + { + panels: {}, + isFullScreenMode: false, + filters: [], + useMargins: true, + ...initialInput, + }, + { embeddableLoaded: {} }, + embeddableFactories, + parent + ); + } + + protected createNewPanelState< + TEmbeddableInput extends EmbeddableInput, + TEmbeddable extends IEmbeddable + >( + factory: EmbeddableFactory, + partial: Partial = {} + ): DashboardPanelState { + const panelState = super.createNewPanelState(factory, partial); + return createPanelState(panelState, Object.values(this.input.panels)); + } + + public render(dom: HTMLElement) { + ReactDOM.render( + // @ts-ignore - hitting https://github.com/DefinitelyTyped/DefinitelyTyped/issues/27805 + + + , + dom + ); + } + + public getPanelIndexPatterns() { + const indexPatterns: IndexPattern[] = []; + Object.values(this.children).forEach(embeddable => { + if (!isErrorEmbeddable(embeddable)) { + const embeddableIndexPatterns = embeddable.getOutput().indexPatterns; + if (embeddableIndexPatterns) { + indexPatterns.push(...embeddableIndexPatterns); + } + } + }); + return indexPatterns; + } + + protected getInheritedInput(id: string): InheritedChildInput { + const { viewMode, refreshConfig, timeRange, query, hidePanelTitles, filters } = this.input; + return { + filters, + hidePanelTitles, + query, + timeRange, + refreshConfig, + viewMode, + id, + }; + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts new file mode 100644 index 000000000000..648204bf987e --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts @@ -0,0 +1,80 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { SavedObjectMetaData } from 'ui/saved_objects/components/saved_object_finder'; +import { SavedObjectAttributes } from 'target/types/server'; +import { + ContainerOutput, + embeddableFactories, + EmbeddableFactory, + ErrorEmbeddable, + Container, +} from '../../../embeddable_api/public'; +import { DashboardContainer, DashboardContainerInput } from './dashboard_container'; + +export const DASHBOARD_CONTAINER_TYPE = 'dashboard'; + +export class DashboardContainerFactory extends EmbeddableFactory< + DashboardContainerInput, + ContainerOutput +> { + public readonly isContainerType = true; + public readonly type = DASHBOARD_CONTAINER_TYPE; + private allowEditing: boolean; + + constructor({ + savedObjectMetaData, + capabilities, + }: { + savedObjectMetaData?: SavedObjectMetaData; + capabilities: { + showWriteControls: boolean; + createNew: boolean; + }; + }) { + super({ savedObjectMetaData }); + this.allowEditing = capabilities.createNew && capabilities.showWriteControls; + } + + public isEditable() { + return this.allowEditing; + } + + public getDisplayName() { + return i18n.translate('dashboardEmbeddableContainer.factory.displayName', { + defaultMessage: 'dashboard', + }); + } + + public getDefaultInput(): Partial { + return { + panels: {}, + isFullScreenMode: false, + useMargins: true, + }; + } + + public async create( + initialInput: DashboardContainerInput, + parent?: Container + ): Promise { + return new DashboardContainer(initialInput, embeddableFactories, parent); + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss new file mode 100644 index 000000000000..24b813ec5896 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss @@ -0,0 +1,127 @@ +// SASSTODO: Can't find this selector, but could break something if removed +.react-grid-layout .gs-w { + z-index: auto; +} + +/** + * 1. Due to https://github.com/STRML/react-grid-layout/issues/240 we have to manually hide the resizable + * element. + */ +.dshLayout--viewing { + .react-resizable-handle { + display: none; /* 1 */ + } +} + +/** + * 1. If we don't give the resizable handler a larger z index value the layout will hide it. + */ +.dshLayout--editing { + .react-resizable-handle { + @include size($euiSizeL); + z-index: $euiZLevel1; /* 1 */ + right: 0; + bottom: 0; + padding-right: $euiSizeS; + padding-bottom: $euiSizeS; + } +} + +/** + * 1. Need to override the react grid layout height when a single panel is expanded. Important is required because + * otherwise the height is set inline. + */ + .dshLayout-isMaximizedPanel { + height: 100% !important; /* 1. */ + width: 100%; + position: absolute; +} + +/** + * .dshLayout-withoutMargins only affects the panel styles themselves, see ../panel + */ + +/** + * When a single panel is expanded, all the other panels are hidden in the grid. + */ +.dshDashboardGrid__item--hidden { + display: none; +} + +/** + * 1. We need to mark this as important because react grid layout sets the width and height of the panels inline. + */ +.dshDashboardGrid__item--expanded { + height: 100% !important; /* 1 */ + width: 100% !important; /* 1 */ + top: 0 !important; /* 1 */ + left: 0 !important; /* 1 */ + + // Altered panel styles can be found in ../panel +} + +// REACT-GRID + +.react-grid-item { + /** + * Disable transitions from the library on each grid element. + */ + transition: none; + /** + * Copy over and overwrite the fill color with EUI color mixin (for theming) + */ + > .react-resizable-handle { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Cpolygon fill='#{hexToRGB($euiColorDarkShade)}' points='6 6 0 6 0 4.2 4 4.2 4.2 4.2 4.2 0 6 0' /%3E%3C/svg%3E%0A"); + + &::after { + border: none; + } + + &:hover, + &:focus { + background-color: $embEditingModeHoverColor; + } + } + + /** + * Dragged/Resized panels in dashboard should always appear above other panels + * and above the placeholder + */ + &.resizing, + &.react-draggable-dragging { + z-index: $euiZLevel2 !important; + } + + &.react-draggable-dragging { + transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance; + @include euiBottomShadowLarge; + border-radius: $euiBorderRadius; // keeps shadow within bounds + } + + /** + * Overwrites red coloring that comes from this library by default. + */ + &.react-grid-placeholder { + border-radius: $euiBorderRadius; + background: $euiColorWarning; + } +} + +// When in view-mode only, and on tiny mobile screens, just stack each of the grid-items + +@include euiBreakpoint('xs', 's') { + .dshLayout--viewing { + .react-grid-item { + position: static !important; + width: calc(100% - #{$euiSize}) !important; + margin: $euiSizeS; + } + + &.dshLayout-withoutMargins { + .react-grid-item { + width: 100% !important; + margin: 0; + } + } + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_index.scss new file mode 100644 index 000000000000..c4a91227fd6b --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/_index.scss @@ -0,0 +1,3 @@ +@import 'src/legacy/core_plugins/embeddable_api/public/variables'; + +@import './dashboard_grid'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx new file mode 100644 index 000000000000..88525669f2e6 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx @@ -0,0 +1,138 @@ +/* + * 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 '../../np_core.test.mocks'; + +import React from 'react'; +import { shallowWithIntl, nextTick, mountWithIntl } from 'test_utils/enzyme_helpers'; +// @ts-ignore +import sizeMe from 'react-sizeme'; + +import { skip } from 'rxjs/operators'; + +import { EmbeddableFactory } from '../../../../embeddable_api/public'; +import { + ContactCardEmbeddableFactory, + CONTACT_CARD_EMBEDDABLE, +} from '../../../../embeddable_api/public/test_samples'; + +import { DashboardGrid, DashboardGridProps } from './dashboard_grid'; +import { DashboardContainer } from '../dashboard_container'; +import { getSampleDashboardInput } from '../../test_helpers'; + +let dashboardContainer: DashboardContainer | undefined; + +function getProps(props?: Partial): DashboardGridProps { + const embeddableFactories = new Map(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + dashboardContainer = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '1': { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { firstName: 'Bob', id: '1' }, + }, + '2': { + gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' }, + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { firstName: 'Stacey', id: '2' }, + }, + }, + }), + embeddableFactories + ); + const defaultTestProps: DashboardGridProps = { + 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(); + const panelElements = component.find('InjectIntl(EmbeddableChildPanelUi)'); + expect(panelElements.length).toBe(2); +}); + +test('renders DashboardGrid with no visualizations', async () => { + const props = getProps(); + const component = shallowWithIntl(); + props.container.updateInput({ panels: {} }); + await nextTick(); + component.update(); + expect(component.find('InjectIntl(EmbeddableChildPanelUi)').length).toBe(0); +}); + +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(EmbeddableChildPanelUi)'); + expect(panelElements.length).toBe(1); +}); + +test('DashboardGrid renders expanded panel', async () => { + const props = getProps(); + const component = shallowWithIntl(); + props.container.updateInput({ expandedPanelId: '1' }); + await nextTick(); + component.update(); + // Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized. + expect(component.find('InjectIntl(EmbeddableChildPanelUi)').length).toBe(2); + + expect((component.state() as { expandedPanelId?: string }).expandedPanelId).toBe('1'); + + props.container.updateInput({ expandedPanelId: undefined }); + await nextTick(); + component.update(); + expect(component.find('InjectIntl(EmbeddableChildPanelUi)').length).toBe(2); + + expect((component.state() as { expandedPanelId?: string }).expandedPanelId).toBeUndefined(); +}); + +test('DashboardGrid unmount unsubscribes', async done => { + const props = getProps(); + const component = mountWithIntl(); + component.unmount(); + + props.container + .getInput$() + .pipe(skip(1)) + .subscribe(() => { + done(); + }); + + props.container.updateInput({ expandedPanelId: '1' }); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.tsx new file mode 100644 index 000000000000..dae5df3f85b0 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.tsx @@ -0,0 +1,296 @@ +/* + * 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 { injectI18n } from '@kbn/i18n/react'; +import classNames from 'classnames'; +import _ from 'lodash'; +import React from 'react'; +import ReactGridLayout, { Layout } from 'react-grid-layout'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; + +// @ts-ignore +import sizeMe from 'react-sizeme'; +import { toastNotifications } from 'ui/notify'; +import { Subscription } from 'rxjs'; +import { DashboardConstants } from '../../../../kibana/public/dashboard/dashboard_constants'; +import { ViewMode, EmbeddableChildPanel } from '../../../../embeddable_api/public'; +import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; +import { DashboardContainer } from '../dashboard_container'; +import { DashboardPanelState, GridData } from '../types'; + +let lastValidGridSize = 0; + +/** + * This is a fix for a bug that stopped the browser window from automatically scrolling down when panels were made + * taller than the current grid. + * see https://github.com/elastic/kibana/issues/14710. + */ +function ensureWindowScrollsToBottom(event: { clientY: number; pageY: number }) { + // The buffer is to handle the case where the browser is maximized and it's impossible for the mouse to move below + // the screen, out of the window. see https://github.com/elastic/kibana/issues/14737 + const WINDOW_BUFFER = 10; + if (event.clientY > window.innerHeight - WINDOW_BUFFER) { + window.scrollTo(0, event.pageY + WINDOW_BUFFER - window.innerHeight); + } +} + +function ResponsiveGrid({ + size, + isViewMode, + layout, + onLayoutChange, + children, + maximizedPanelId, + useMargins, +}: { + size: { width: number }; + isViewMode: boolean; + layout: Layout[]; + onLayoutChange: () => void; + children: JSX.Element[]; + maximizedPanelId: string; + useMargins: boolean; +}) { + // This is to prevent a bug where view mode changes when the panel is expanded. View mode changes will trigger + // the grid to re-render, but when a panel is expanded, the size will be 0. Minimizing the panel won't cause the + // grid to re-render so it'll show a grid with a width of 0. + lastValidGridSize = size.width > 0 ? size.width : lastValidGridSize; + const classes = classNames({ + 'dshLayout--viewing': isViewMode, + 'dshLayout--editing': !isViewMode, + 'dshLayout-isMaximizedPanel': maximizedPanelId !== undefined, + 'dshLayout-withoutMargins': !useMargins, + }); + + const MARGINS = useMargins ? 8 : 0; + // We can't take advantage of isDraggable or isResizable due to performance concerns: + // https://github.com/STRML/react-grid-layout/issues/240 + return ( + ensureWindowScrollsToBottom(event)} + > + {children} + + ); +} + +// Using sizeMe sets up the grid to be re-rendered automatically not only when the window size changes, but also +// when the container size changes, so it works for Full Screen mode switches. +const config = { monitorWidth: true }; +const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid); + +export interface DashboardGridProps extends ReactIntl.InjectedIntlProps { + container: DashboardContainer; +} + +interface State { + focusedPanelIndex?: string; + isLayoutInvalid: boolean; + layout?: GridData[]; + panels: { [key: string]: DashboardPanelState }; + viewMode: ViewMode; + useMargins: boolean; + expandedPanelId?: string; +} + +interface PanelLayout extends Layout { + i: string; +} + +class DashboardGridUi extends React.Component { + private subscription?: Subscription; + private mounted: boolean = false; + // A mapping of panelIndexes to grid items so we can set the zIndex appropriately on the last focused + // item. + private gridItems = {} as { [key: string]: HTMLDivElement | null }; + + constructor(props: DashboardGridProps) { + super(props); + + this.state = { + layout: [], + isLayoutInvalid: false, + focusedPanelIndex: undefined, + panels: this.props.container.getInput().panels, + viewMode: this.props.container.getInput().viewMode, + useMargins: this.props.container.getInput().useMargins, + expandedPanelId: this.props.container.getInput().expandedPanelId, + }; + } + + public componentDidMount() { + this.mounted = true; + let isLayoutInvalid = false; + let layout; + try { + layout = this.buildLayoutFromPanels(); + } catch (error) { + console.error(error); // eslint-disable-line no-console + + isLayoutInvalid = true; + toastNotifications.addDanger({ + title: this.props.intl.formatMessage({ + id: 'dashboardEmbeddableContainer.dashboardGrid.toast.unableToLoadDashboardDangerMessage', + defaultMessage: 'Unable to load dashboard.', + }), + text: error.message, + }); + window.location.hash = DashboardConstants.LANDING_PAGE_PATH; + } + this.setState({ + layout, + isLayoutInvalid, + }); + + this.subscription = this.props.container.getInput$().subscribe(input => { + if (this.mounted) { + this.setState({ + panels: input.panels, + viewMode: input.viewMode, + useMargins: input.useMargins, + expandedPanelId: input.expandedPanelId, + }); + } + }); + } + + public componentWillUnmount() { + this.mounted = false; + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public buildLayoutFromPanels = (): GridData[] => { + return _.map(this.state.panels, panel => { + return panel.gridData; + }); + }; + + public onLayoutChange = (layout: PanelLayout[]) => { + const panels = this.state.panels; + const updatedPanels: { [key: string]: DashboardPanelState } = layout.reduce( + (updatedPanelsAcc, panelLayout) => { + updatedPanelsAcc[panelLayout.i] = { + ...panels[panelLayout.i], + gridData: _.pick(panelLayout, ['x', 'y', 'w', 'h', 'i']), + }; + return updatedPanelsAcc; + }, + {} as { [key: string]: DashboardPanelState } + ); + this.onPanelsUpdated(updatedPanels); + }; + + public onPanelsUpdated = (panels: { [key: string]: DashboardPanelState }) => { + this.props.container.updateInput({ + panels, + }); + }; + + public onPanelFocused = (focusedPanelIndex: string): void => { + this.setState({ focusedPanelIndex }); + }; + + public onPanelBlurred = (blurredPanelIndex: string): void => { + if (this.state.focusedPanelIndex === blurredPanelIndex) { + this.setState({ focusedPanelIndex: undefined }); + } + }; + + public renderDOM() { + const { focusedPanelIndex, panels, expandedPanelId } = this.state; + + // Part of our unofficial API - need to render in a consistent order for plugins. + const panelsInOrder = Object.keys(panels).map( + (key: string) => panels[key] as DashboardPanelState + ); + panelsInOrder.sort((panelA, panelB) => { + if (panelA.gridData.y === panelB.gridData.y) { + return panelA.gridData.x - panelB.gridData.x; + } else { + return panelA.gridData.y - panelB.gridData.y; + } + }); + + return _.map(panelsInOrder, panel => { + const expandPanel = + expandedPanelId !== undefined && expandedPanelId === panel.explicitInput.id; + const hidePanel = expandedPanelId !== undefined && expandedPanelId !== panel.explicitInput.id; + const classes = classNames({ + 'dshDashboardGrid__item--expanded': expandPanel, + 'dshDashboardGrid__item--hidden': hidePanel, + }); + return ( +
{ + this.gridItems[panel.explicitInput.id] = reactGridItem; + }} + > + +
+ ); + }); + } + + public render() { + if (this.state.isLayoutInvalid) { + return null; + } + + const { viewMode } = this.state; + const isViewMode = viewMode === ViewMode.VIEW; + return ( + + {this.renderDOM()} + + ); + } +} + +export const DashboardGrid = injectI18n(DashboardGridUi); diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/index.ts new file mode 100644 index 000000000000..ca62a1c83ff5 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/grid/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 { DashboardGrid } from './dashboard_grid'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/index.ts new file mode 100644 index 000000000000..e294d908f6c6 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { DASHBOARD_CONTAINER_TYPE, DashboardContainerFactory } from './dashboard_container_factory'; +export { DashboardContainer, DashboardContainerInput } from './dashboard_container'; +export { createPanelState } from './panel'; + +export { DashboardPanelState } from './types'; + +export { + DASHBOARD_GRID_COLUMN_COUNT, + DEFAULT_PANEL_HEIGHT, + DEFAULT_PANEL_WIDTH, +} from './dashboard_constants'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_dashboard_panel.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_dashboard_panel.scss new file mode 100644 index 000000000000..48961110db48 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_dashboard_panel.scss @@ -0,0 +1,24 @@ +/** + * EDITING MODE + * Use .dshLayout--editing to target editing state because + * .embPanel--editing doesn't get updating without a hard refresh + */ + +// LAYOUT MODES + +// Adjust borders/etc... for non-spaced out and expanded panels +.dshLayout-withoutMargins, +.dshDashboardGrid__item--expanded { + .embPanel { + box-shadow: none; + border-radius: 0; + } +} + +// Remove border color unless in editing mode +.dshLayout-withoutMargins:not(.dshLayout--editing), +.dshDashboardGrid__item--expanded { + .embPanel { + border-color: transparent; + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_index.scss new file mode 100644 index 000000000000..b899f02b2639 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/_index.scss @@ -0,0 +1 @@ +@import "./dashboard_panel"; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/create_panel_state.test.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/create_panel_state.test.ts new file mode 100644 index 000000000000..711e722804f4 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/create_panel_state.test.ts @@ -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. + */ + +import '../../np_core.test.mocks'; + +import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants'; +import { DashboardPanelState } from '../types'; +import { createPanelState } from './create_panel_state'; +import { CONTACT_CARD_EMBEDDABLE } from '../../../../embeddable_api/public/test_samples'; +import { EmbeddableInput } from '../../../../embeddable_api/public'; + +interface TestInput extends EmbeddableInput { + test: string; +} +const panels: DashboardPanelState[] = []; + +test('createPanelState adds a new panel state in 0,0 position', () => { + const panelState = createPanelState( + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'hi', id: '123' }, + }, + [] + ); + expect(panelState.explicitInput.test).toBe('hi'); + expect(panelState.type).toBe(CONTACT_CARD_EMBEDDABLE); + expect(panelState.explicitInput.id).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( + { type: CONTACT_CARD_EMBEDDABLE, explicitInput: { test: 'bye', id: '456' } }, + 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( + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'bye', id: '789' }, + }, + 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( + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'bye', id: '987' }, + }, + 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_container/public/embeddable/panel/create_panel_state.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/create_panel_state.ts new file mode 100644 index 000000000000..c5c012f9a4b5 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/create_panel_state.ts @@ -0,0 +1,119 @@ +/* + * 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 _ from 'lodash'; + +import { PanelState, EmbeddableInput } from '../../../../embeddable_api/public'; +import { + DASHBOARD_GRID_COLUMN_COUNT, + DEFAULT_PANEL_HEIGHT, + DEFAULT_PANEL_WIDTH, +} from '../dashboard_constants'; +import { DashboardPanelState } from '../types'; + +// Look for the smallest y and x value where the default panel will fit. +function findTopLeftMostOpenSpace( + width: number, + height: number, + currentPanels: DashboardPanelState[] +) { + let maxY = -1; + + currentPanels.forEach(panel => { + maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY); + }); + + // Handle case of empty grid. + if (maxY < 0) { + return { x: 0, y: 0 }; + } + + const grid = new Array(maxY); + for (let y = 0; y < maxY; y++) { + grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0); + } + + currentPanels.forEach(panel => { + for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) { + for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) { + const row = grid[y]; + if (row === undefined) { + throw new Error( + `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify( + panel + )}` + ); + } + grid[y][x] = 1; + } + } + }); + + for (let y = 0; y < maxY; y++) { + for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) { + if (grid[y][x] === 1) { + // Space is filled + continue; + } else { + for (let h = y; h < Math.min(y + height, maxY); h++) { + for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) { + const spaceIsEmpty = grid[h][w] === 0; + const fitsPanelWidth = w === x + width - 1; + // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence + // we check the minimum of maxY and the panel height. + const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1); + + if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) { + // Found space + return { x, y }; + } else if (grid[h][w] === 1) { + // x, y spot doesn't work, break. + break; + } + } + } + } + } + } + return { x: 0, y: maxY }; +} + +/** + * Creates and initializes a basic panel state. + */ +export function createPanelState( + panelState: PanelState, + currentPanels: DashboardPanelState[] +): DashboardPanelState { + const { x, y } = findTopLeftMostOpenSpace( + DEFAULT_PANEL_WIDTH, + DEFAULT_PANEL_HEIGHT, + currentPanels + ); + return { + gridData: { + w: DEFAULT_PANEL_WIDTH, + h: DEFAULT_PANEL_HEIGHT, + x, + y, + i: panelState.explicitInput.id, + }, + ...panelState, + }; +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/index.ts new file mode 100644 index 000000000000..2eb223018c73 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/panel/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 { createPanelState } from './create_panel_state'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/types.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/types.ts new file mode 100644 index 000000000000..3a8d1d4a7be1 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/types.ts @@ -0,0 +1,34 @@ +/* + * 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 { PanelState, EmbeddableInput } from '../../../embeddable_api/public/index'; +export type PanelId = string; +export type SavedObjectId = string; + +export interface GridData { + w: number; + h: number; + x: number; + y: number; + i: string; +} + +export interface DashboardPanelState + extends PanelState { + readonly gridData: GridData; +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss new file mode 100644 index 000000000000..7cbe13511587 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss @@ -0,0 +1,8 @@ +.dshDashboardViewport { + width: 100%; + background-color: $euiColorEmptyShade; +} + +.dshDashboardViewport-withMargins { + width: 100%; +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_index.scss new file mode 100644 index 000000000000..56483d9d1019 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/_index.scss @@ -0,0 +1 @@ +@import './dashboard_viewport'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx new file mode 100644 index 000000000000..65604983f46d --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx @@ -0,0 +1,133 @@ +/* + * 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 '../../np_core.test.mocks'; + +import React from 'react'; +import { skip } from 'rxjs/operators'; +import { mount } from 'enzyme'; + +import { I18nProvider } from '@kbn/i18n/react'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +import { + ContactCardEmbeddableFactory, + CONTACT_CARD_EMBEDDABLE, +} from '../../../../embeddable_api/public/test_samples'; +import { EmbeddableFactory } from '../../../../embeddable_api/public'; + +import { DashboardViewport, DashboardViewportProps } from './dashboard_viewport'; +import { DashboardContainer } from '../dashboard_container'; +import { getSampleDashboardInput } from '../../test_helpers'; + +let dashboardContainer: DashboardContainer | undefined; + +function getProps(props?: Partial): DashboardViewportProps { + const embeddableFactories = new Map(); + embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); + dashboardContainer = new DashboardContainer( + getSampleDashboardInput({ + panels: { + '1': { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { firstName: 'Bob', id: '1' }, + }, + '2': { + gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' }, + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { firstName: 'Stacey', id: '2' }, + }, + }, + }), + embeddableFactories + ); + const defaultTestProps: DashboardViewportProps = { + container: dashboardContainer, + }; + return Object.assign(defaultTestProps, props); +} + +test('renders DashboardViewport', () => { + const props = getProps(); + const component = mount( + + + + ); + const panels = findTestSubject(component, 'dashboardPanel'); + expect(panels.length).toBe(2); +}); + +test('renders DashboardViewport with no visualizations', () => { + const props = getProps(); + props.container.updateInput({ panels: {} }); + const component = mount( + + + + ); + const panels = findTestSubject(component, 'dashboardPanel'); + expect(panels.length).toBe(0); + + component.unmount(); +}); + +test('renders exit full screen button when in full screen mode', async () => { + const props = getProps(); + props.container.updateInput({ isFullScreenMode: true }); + const component = mount( + + + + ); + let exitButton = findTestSubject(component, 'exitFullScreenModeText'); + expect(exitButton.length).toBe(1); + + props.container.updateInput({ isFullScreenMode: false }); + + await nextTick(); + component.update(); + + exitButton = findTestSubject(component, 'exitFullScreenModeText'); + expect(exitButton.length).toBe(0); + + component.unmount(); +}); + +test('DashboardViewport unmount unsubscribes', async done => { + const props = getProps(); + const component = mount( + + + + ); + component.unmount(); + + props.container + .getInput$() + .pipe(skip(1)) + .subscribe(() => { + done(); + }); + + props.container.updateInput({ panels: {} }); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.tsx new file mode 100644 index 000000000000..61f23a886351 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.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 React from 'react'; +// @ts-ignore +import { ExitFullScreenButton } from 'ui/exit_full_screen'; +import { Subscription } from 'rxjs'; + +import { PanelState } from '../../../../embeddable_api/public'; + +import { DashboardContainer } from '../dashboard_container'; +import { DashboardGrid } from '../grid'; + +export interface DashboardViewportProps { + container: DashboardContainer; +} + +interface State { + isFullScreenMode: boolean; + useMargins: boolean; + title: string; + description?: string; + panels: { [key: string]: PanelState }; +} + +export class DashboardViewport extends React.Component { + private subscription?: Subscription; + private mounted: boolean = false; + constructor(props: DashboardViewportProps) { + super(props); + const { isFullScreenMode, panels, useMargins, title } = this.props.container.getInput(); + + this.state = { + isFullScreenMode, + panels, + useMargins, + title, + }; + } + + public componentDidMount() { + this.mounted = true; + this.subscription = this.props.container.getInput$().subscribe(() => { + const { isFullScreenMode, useMargins, title, description } = this.props.container.getInput(); + if (this.mounted) { + this.setState({ + isFullScreenMode, + description, + useMargins, + title, + }); + } + }); + } + + public componentWillUnmount() { + this.mounted = false; + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public onExitFullScreenMode = () => { + this.props.container.updateInput({ + isFullScreenMode: false, + }); + }; + + public render() { + const { container } = this.props; + return ( +
+ {this.state.isFullScreenMode && ( + + )} + +
+ ); + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss new file mode 100644 index 000000000000..21a82c782ee5 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss @@ -0,0 +1,6 @@ + +@import 'src/legacy/ui/public/styles/styling_constants'; + +// TODO: uncomment once the duplicate styles are removed from the dashboard app itself. +// MUST STAY AT THE BOTTOM BECAUSE OF DARK THEME IMPORTS +// @import './embeddable/index'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts new file mode 100644 index 000000000000..ab594f858d7f --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts @@ -0,0 +1,31 @@ +/* + * 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 'uiExports/embeddableActions'; +import 'uiExports/embeddableFactories'; + +export { + DASHBOARD_GRID_COLUMN_COUNT, + DEFAULT_PANEL_HEIGHT, + DEFAULT_PANEL_WIDTH, + DashboardContainer, + DashboardContainerInput, + DASHBOARD_CONTAINER_TYPE, + DashboardContainerFactory, + DashboardPanelState, +} from './embeddable'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts new file mode 100644 index 000000000000..a3909bc556b5 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts @@ -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 { fatalErrorsServiceMock, notificationServiceMock } from '../../../../core/public/mocks'; + +let modalContents: React.Component; + +export const getModalContents = () => modalContents; + +jest.doMock('ui/new_platform', () => { + return { + npStart: { + core: { + overlays: { + openFlyout: jest.fn(), + openModal: (component: React.Component) => { + modalContents = component; + return { + close: jest.fn(), + }; + }, + }, + }, + }, + npSetup: { + core: { + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + notifications: notificationServiceMock.createSetupContract(), + }, + }, + }; +}); + +jest.doMock('ui/metadata', () => ({ + metadata: { + branch: 'my-metadata-branch', + version: 'my-metadata-version', + }, +})); + +jest.doMock('ui/capabilities', () => ({ + uiCapabilities: { + visualize: { + save: true, + }, + }, +})); + +jest.doMock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0', setVisible: () => {} })); diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/shim/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/shim/index.ts new file mode 100644 index 000000000000..7868ce7f06cf --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/shim/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. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart } from 'ui/new_platform'; +import { embeddablePlugin } from '../../../embeddable_api/public'; +import { Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + const dashboardContainerPlugin = new Plugin(initializerContext); + + dashboardContainerPlugin.setup(npSetup.core, { + embeddable: embeddablePlugin, + }); + + dashboardContainerPlugin.start(npStart.core, { + embeddable: embeddablePlugin, + }); +} + +plugin({} as any); diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/shim/plugin.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/shim/plugin.ts new file mode 100644 index 000000000000..ebc65a925ad1 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/shim/plugin.ts @@ -0,0 +1,48 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/public'; +import { EmbeddablePlugin, CONTEXT_MENU_TRIGGER } from '../../../embeddable_api/public'; +import { ExpandPanelAction, EXPAND_PANEL_ACTION } from '../actions'; +import { DashboardContainerFactory } from '../embeddable'; + +export class Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: { embeddable: EmbeddablePlugin }) { + plugins.embeddable.addAction(new ExpandPanelAction()); + plugins.embeddable.attachAction({ + triggerId: CONTEXT_MENU_TRIGGER, + actionId: EXPAND_PANEL_ACTION, + }); + } + + public start(core: CoreStart, plugins: { embeddable: EmbeddablePlugin }) { + plugins.embeddable.addEmbeddableFactory( + new DashboardContainerFactory({ + capabilities: core.application.capabilities.dashboard as { + showWriteControls: boolean; + createNew: boolean; + }, + }) + ); + } + + public stop() {} +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/get_sample_dashboard_input.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/get_sample_dashboard_input.ts new file mode 100644 index 000000000000..cb063f765546 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/get_sample_dashboard_input.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 { ViewMode, EmbeddableInput } from '../../../embeddable_api/public'; +import { DashboardContainerInput, DashboardPanelState } from '../embeddable'; + +export function getSampleDashboardInput( + overrides?: Partial +): DashboardContainerInput { + return { + id: '123', + filters: [], + useMargins: false, + isFullScreenMode: false, + title: 'My Dashboard', + query: { + language: 'kuery', + query: 'hi', + }, + timeRange: { + to: 'now', + from: 'now-15m', + }, + viewMode: ViewMode.VIEW, + panels: {}, + ...overrides, + }; +} + +export function getSampleDashboardPanel( + overrides: Partial> & { + explicitInput: { id: string }; + type: string; + } +): DashboardPanelState { + return { + gridData: { + h: 15, + w: 15, + x: 0, + y: 0, + i: overrides.explicitInput.id, + }, + type: overrides.type, + explicitInput: overrides.explicitInput, + ...overrides, + }; +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/index.ts new file mode 100644 index 000000000000..88909826971f --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/test_helpers/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/embeddable_api/public/containers/i_container.ts b/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts index fb87062af1fd..c09f48e77c31 100644 --- a/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts +++ b/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts @@ -27,9 +27,7 @@ import { import { IEmbeddable } from '../embeddables/i_embeddable'; export interface PanelState< - E extends { [key: string]: unknown } & { id: string } = { [key: string]: unknown } & { - id: string; - } + E extends { id: string; [key: string]: unknown } = { id: string; [key: string]: unknown } > { savedObjectId?: string; @@ -40,7 +38,7 @@ export interface PanelState< // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input // will be derived from the container's input. **Any state in here will override any state derived from // the container.** - explicitInput: E; + explicitInput: Partial & { id: string }; } export interface ContainerOutput extends EmbeddableOutput { diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts index 0fa7724e7ecc..be868c5dd496 100644 --- a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts @@ -77,7 +77,7 @@ export abstract class EmbeddableFactory< /** * Returns whether the current user should be allowed to edit this type of - * embeddable. + * embeddable. Most of the time this should be based off the capabilities service. */ public abstract isEditable(): boolean; diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/i_embeddable.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/i_embeddable.ts index 066b47a01fab..daf4587bb549 100644 --- a/src/legacy/core_plugins/embeddable_api/public/embeddables/i_embeddable.ts +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/i_embeddable.ts @@ -21,7 +21,12 @@ import { Adapters } from 'ui/inspector'; import { Observable } from 'rxjs'; import { IContainer } from '../containers'; import { ViewMode } from '../types'; -export interface EmbeddableInput { + +interface TIndexSignature { + [key: string]: unknown; +} + +export interface EmbeddableInput extends TIndexSignature { viewMode?: ViewMode; title?: string; id: string; diff --git a/src/legacy/core_plugins/embeddable_api/public/index.ts b/src/legacy/core_plugins/embeddable_api/public/index.ts index 92cfe0a369c2..10dc81817c86 100644 --- a/src/legacy/core_plugins/embeddable_api/public/index.ts +++ b/src/legacy/core_plugins/embeddable_api/public/index.ts @@ -29,7 +29,7 @@ export { isErrorEmbeddable, } from './embeddables'; -export { ViewMode, Trigger } from './types'; +export { ViewMode, Trigger, EmbeddablePlugin } from './types'; export { actionRegistry, Action, ActionContext, IncompatibleActionError } from './actions'; @@ -51,3 +51,5 @@ export { } from './containers'; export { AddPanelAction, EmbeddablePanel, openAddPanelFlyout } from './panel'; + +export { embeddablePlugin } from './plugin'; diff --git a/src/legacy/core_plugins/embeddable_api/public/plugin.ts b/src/legacy/core_plugins/embeddable_api/public/plugin.ts new file mode 100644 index 000000000000..e4263501a104 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/plugin.ts @@ -0,0 +1,30 @@ +/* + * 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 } from './actions'; +import { EmbeddableFactory, embeddableFactories } from './embeddables'; +import { EmbeddablePlugin } from './types'; +import { attachAction, triggerRegistry } from './triggers'; + +export const embeddablePlugin: EmbeddablePlugin = { + addAction: (action: Action) => actionRegistry.set(action.id, action), + addEmbeddableFactory: (factory: EmbeddableFactory) => + embeddableFactories.set(factory.type, factory), + attachAction: data => attachAction(triggerRegistry, data), +}; diff --git a/src/legacy/core_plugins/embeddable_api/public/types.ts b/src/legacy/core_plugins/embeddable_api/public/types.ts index 62fcbc82cbdb..935767dec579 100644 --- a/src/legacy/core_plugins/embeddable_api/public/types.ts +++ b/src/legacy/core_plugins/embeddable_api/public/types.ts @@ -17,6 +17,9 @@ * under the License. */ +import { Action } from './actions'; +import { EmbeddableFactory } from './embeddables'; + export interface Trigger { id: string; title?: string; @@ -40,3 +43,9 @@ export enum ViewMode { EDIT = 'edit', VIEW = 'view', } + +export interface EmbeddablePlugin { + addAction: (action: Action) => void; + addEmbeddableFactory: (factory: EmbeddableFactory) => void; + attachAction: (data: { triggerId: string; actionId: string }) => void; +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts index 3dcbf769ed25..c409037a1269 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/index.ts @@ -36,6 +36,7 @@ export default function(kibana: any) { ], embeddableFactories: [ 'plugins/embeddable_api/test_samples/embeddables/hello_world/hello_world_embeddable_factory', + 'plugins/embeddable_api/test_samples/embeddables/contact_card/contact_card_embeddable_factory', ], }, init(server: Legacy.Server) { 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 index a9ec0095b5ea..926d7c8c333c 100644 --- 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 @@ -22,6 +22,7 @@ import React, { Component } from 'react'; import { EmbeddableFactory } from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; import { ContactCardEmbeddableExample } from './hello_world_embeddable_example'; import { HelloWorldContainerExample } from './hello_world_container_example'; +import { DashboardContainerExample } from './dashboard_container_example'; export interface AppProps { embeddableFactories: Map; @@ -40,6 +41,10 @@ export class App extends Component { id: 'helloWorldEmbeddable', name: 'Hello World Embeddable', }, + { + id: 'dashboardContainer', + name: 'Dashboard Container', + }, ]; this.state = { @@ -85,6 +90,9 @@ export class App extends Component { case 'helloWorldEmbeddable': { return ; } + case 'dashboardContainer': { + 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 000000000000..a492b7165f85 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_container_example.tsx @@ -0,0 +1,100 @@ +/* + * 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 { EuiButton, EuiLoadingChart } from '@elastic/eui'; +import { + DASHBOARD_CONTAINER_TYPE, + DashboardContainer, + DashboardContainerFactory, +} from 'plugins/dashboard_embeddable_container'; + +import { + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, + EmbeddablePanel, + embeddableFactories, +} from 'plugins/embeddable_api'; + +import { dashboardInput } from './dashboard_input'; + +interface State { + loaded: boolean; + viewMode: ViewMode; +} + +export class DashboardContainerExample extends React.Component<{}, State> { + private mounted = false; + private container: DashboardContainer | ErrorEmbeddable | undefined; + + public constructor() { + super({}); + this.state = { + viewMode: ViewMode.VIEW, + loaded: false, + }; + } + + public async componentDidMount() { + this.mounted = true; + const dashboardFactory = embeddableFactories.get( + DASHBOARD_CONTAINER_TYPE + ) as DashboardContainerFactory; + if (dashboardFactory) { + this.container = await dashboardFactory.create(dashboardInput); + if (this.mounted) { + this.setState({ loaded: true }); + } + } + } + + public componentWillUnmount() { + this.mounted = false; + if (this.container) { + this.container.destroy(); + } + } + + public switchViewMode = () => { + this.setState((prevState: State) => { + if (!this.container || isErrorEmbeddable(this.container)) { + return prevState; + } + const newMode = prevState.viewMode === ViewMode.VIEW ? ViewMode.EDIT : ViewMode.VIEW; + this.container.updateInput({ viewMode: newMode }); + return { viewMode: newMode }; + }); + }; + + public render() { + return ( +
+

Dashboard Container

+ + {this.state.viewMode === ViewMode.VIEW ? 'Edit' : 'View'} + + {!this.state.loaded || !this.container ? ( + + ) : ( + + )} +
+ ); + } +} 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 000000000000..240911848269 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts @@ -0,0 +1,122 @@ +/* + * 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 '../../../../../../src/legacy/core_plugins/dashboard_embeddable_container/public'; +import { + HELLO_WORLD_EMBEDDABLE_TYPE, + CONTACT_CARD_EMBEDDABLE, +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/test_samples'; + +import { ViewMode } from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; + +export const dashboardInput: DashboardContainerInput = { + panels: { + '1': { + gridData: { + w: 24, + h: 15, + x: 0, + y: 15, + i: '1', + }, + type: HELLO_WORLD_EMBEDDABLE_TYPE, + explicitInput: { + id: '1', + }, + }, + '2': { + gridData: { + w: 24, + h: 15, + x: 24, + y: 15, + i: '2', + }, + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { + id: '2', + firstName: 'Sue', + }, + }, + // TODO: Uncomment when saved objects are using the new Embeddable API + // '822cd0f0-ce7c-419d-aeaa-1171cf452745': { + // gridData: { + // w: 24, + // h: 15, + // x: 0, + // y: 0, + // i: '822cd0f0-ce7c-419d-aeaa-1171cf452745', + // }, + // type: 'visualization', + // explicitInput: { + // id: '822cd0f0-ce7c-419d-aeaa-1171cf452745', + // }, + // savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', + // }, + // '66f0a265-7b06-4974-accd-d05f74f7aa82': { + // gridData: { + // w: 24, + // h: 15, + // x: 24, + // y: 0, + // i: '66f0a265-7b06-4974-accd-d05f74f7aa82', + // }, + // type: 'visualization', + // explicitInput: { + // id: '66f0a265-7b06-4974-accd-d05f74f7aa82', + // }, + // savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', + // }, + // 'b2861741-40b9-4dc8-b82b-080c6e29a551': { + // gridData: { + // w: 24, + // h: 15, + // x: 0, + // y: 15, + // i: 'b2861741-40b9-4dc8-b82b-080c6e29a551', + // }, + // type: 'search', + // explicitInput: { + // id: 'b2861741-40b9-4dc8-b82b-080c6e29a551', + // }, + // savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', + // }, + }, + isFullScreenMode: false, + filters: [], + useMargins: true, + id: '', + hidePanelTitles: false, + query: { + query: '', + language: 'kuery', + }, + timeRange: { + from: '2017-10-01T20:20:36.275Z', + to: '2019-02-04T21:20:55.548Z', + }, + refreshConfig: { + value: 0, + pause: true, + }, + viewMode: ViewMode.EDIT, + lastReloadRequestTime: 1556569306103, + title: 'New Dashboard', + description: '', +}; diff --git a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js new file mode 100644 index 000000000000..4700fc8787e7 --- /dev/null +++ b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js @@ -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 expect from '@kbn/expect'; + +export default function ({ getService }) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('dashboard container', () => { + before(async () => { + await testSubjects.click('embedExplorerTab-dashboardContainer'); + }); + + it('hello world embeddable renders', async () => { + await retry.try(async () => { + const text = await testSubjects.getVisibleText('helloWorldEmbeddable'); + expect(text).to.be('HELLO WORLD!'); + }); + }); + + it('contact card embeddable renders', async () => { + await testSubjects.existOrFail('embeddablePanelHeading-HelloSue'); + }); + + // TODO: uncomment when we add saved searches to the test dashboard. + // it('pie charts', async () => { + // await pieChart.expectPieSliceCount(5); + // }); + + // it('markdown', async () => { + // await dashboardExpect.markdownWithValuesExists(['I\'m a markdown!']); + // }); + + // it('saved search', async () => { + // await dashboardExpect.savedSearchRowCount(50); + // }); + }); +} diff --git a/test/plugin_functional/test_suites/embeddable_explorer/index.js b/test/plugin_functional/test_suites/embeddable_explorer/index.js index b6748cede61a..cc2d5a47b645 100644 --- a/test/plugin_functional/test_suites/embeddable_explorer/index.js +++ b/test/plugin_functional/test_suites/embeddable_explorer/index.js @@ -36,5 +36,6 @@ export default function ({ getService, getPageObjects, loadTestFile }) { loadTestFile(require.resolve('./hello_world_container')); loadTestFile(require.resolve('./hello_world_embeddable')); + loadTestFile(require.resolve('./dashboard_container')); }); }