diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/index.ts index cbbbd4182785..1dad1b488590 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/public'; +import { PluginInitializerContext } from '../../../../../../core/public'; import { DashboardEmbeddableContainerPublicPlugin } from './plugin'; export * from './lib'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/index.ts index b0707610cf21..6c0db82fbbc5 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/index.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/index.ts @@ -18,3 +18,4 @@ */ export { ExpandPanelAction, EXPAND_PANEL_ACTION } from './expand_panel_action'; +export { ReplacePanelAction, REPLACE_PANEL_ACTION } from './replace_panel_action'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/open_replace_panel_flyout.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/open_replace_panel_flyout.tsx new file mode 100644 index 000000000000..b6652caf3ab8 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/open_replace_panel_flyout.tsx @@ -0,0 +1,56 @@ +/* + * 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 { CoreStart } from 'src/core/public'; +import { ReplacePanelFlyout } from './replace_panel_flyout'; + +import { + IEmbeddable, + EmbeddableInput, + EmbeddableOutput, +} from '../../../../../../embeddable_api/public/np_ready/public'; + +import { IContainer } from '../../../../../../embeddable_api/public/np_ready/public'; +import { NotificationsStart } from '../../../../../../../../core/public'; + +export async function openReplacePanelFlyout(options: { + embeddable: IContainer; + core: CoreStart; + savedObjectFinder: React.ComponentType; + notifications: NotificationsStart; + panelToRemove: IEmbeddable; +}) { + const { embeddable, core, panelToRemove, savedObjectFinder, notifications } = options; + const flyoutSession = core.overlays.openFlyout( + { + if (flyoutSession) { + flyoutSession.close(); + } + }} + panelToRemove={panelToRemove} + savedObjectsFinder={savedObjectFinder} + notifications={notifications} + />, + { + 'data-test-subj': 'replacePanelFlyout', + } + ); +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_action.test.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_action.test.tsx new file mode 100644 index 000000000000..e1d2e3609570 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_action.test.tsx @@ -0,0 +1,129 @@ +/* + * 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 { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_api'; +import { ReplacePanelAction } from './replace_panel_action'; +import { DashboardContainer } from '../embeddable'; +import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from '../../../../../../embeddable_api/public/np_ready/public/lib/test_samples'; +import { DashboardOptions } from '../embeddable/dashboard_container_factory'; + +const embeddableFactories = new Map(); +embeddableFactories.set( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any) +); + +let container: DashboardContainer; +let embeddable: ContactCardEmbeddable; + +beforeEach(async () => { + const options: DashboardOptions = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: { + getEmbeddableFactory: (id: string) => embeddableFactories.get(id)!, + } as any, + inspector: {} as any, + notifications: {} as any, + overlays: {} as any, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + const input = getSampleDashboardInput({ + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, + }); + container = new DashboardContainer(input, options); + + 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('Executes the replace panel action', async () => { + let core: any; + let SavedObjectFinder: any; + let notifications: any; + const action = new ReplacePanelAction(core, SavedObjectFinder, notifications); + action.execute({ embeddable }); +}); + +test('Is not compatible when embeddable is not in a dashboard container', async () => { + let core: any; + let SavedObjectFinder: any; + let notifications: any; + const action = new ReplacePanelAction(core, SavedObjectFinder, notifications); + expect( + await action.isCompatible({ + embeddable: new ContactCardEmbeddable( + { firstName: 'sue', id: '123' }, + { execAction: (() => null) as any } + ), + }) + ).toBe(false); +}); + +test('Execute throws an error when called with an embeddable not in a parent', async () => { + let core: any; + let SavedObjectFinder: any; + let notifications: any; + const action = new ReplacePanelAction(core, SavedObjectFinder, notifications); + async function check() { + await action.execute({ embeddable: container }); + } + await expect(check()).rejects.toThrow(Error); +}); + +test('Returns title', async () => { + let core: any; + let SavedObjectFinder: any; + let notifications: any; + const action = new ReplacePanelAction(core, SavedObjectFinder, notifications); + expect(action.getDisplayName({ embeddable })).toBeDefined(); +}); + +test('Returns an icon', async () => { + let core: any; + let SavedObjectFinder: any; + let notifications: any; + const action = new ReplacePanelAction(core, SavedObjectFinder, notifications); + expect(action.getIconType({ embeddable })).toBeDefined(); +}); diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_action.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_action.tsx new file mode 100644 index 000000000000..f36efc498b15 --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_action.tsx @@ -0,0 +1,94 @@ +/* + * 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 { CoreStart } from 'src/core/public'; + +import { IEmbeddable, ViewMode } from '../../../../../../embeddable_api/public/np_ready/public'; +import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; +import { + IAction, + IncompatibleActionError, +} from '../../../../../../../../plugins/ui_actions/public'; +import { NotificationsStart } from '../../../../../../../../core/public'; +import { openReplacePanelFlyout } from './open_replace_panel_flyout'; + +export const REPLACE_PANEL_ACTION = 'replacePanel'; + +function isDashboard(embeddable: IEmbeddable): embeddable is DashboardContainer { + return embeddable.type === DASHBOARD_CONTAINER_TYPE; +} + +interface ActionContext { + embeddable: IEmbeddable; +} + +export class ReplacePanelAction implements IAction { + public readonly type = REPLACE_PANEL_ACTION; + public readonly id = REPLACE_PANEL_ACTION; + public order = 11; + + constructor( + private core: CoreStart, + private savedobjectfinder: React.ComponentType, + private notifications: NotificationsStart + ) {} + + public getDisplayName({ embeddable }: ActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + return i18n.translate('dashboardEmbeddableContainer.panel.removePanel.replacePanel', { + defaultMessage: 'Replace panel', + }); + } + + public getIconType({ embeddable }: ActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + return 'kqlOperand'; + } + + public async isCompatible({ embeddable }: ActionContext) { + if (embeddable.getInput().viewMode) { + if (embeddable.getInput().viewMode === ViewMode.VIEW) { + return false; + } + } + + return Boolean(embeddable.parent && isDashboard(embeddable.parent)); + } + + public async execute({ embeddable }: ActionContext) { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + + const view = embeddable; + const dash = embeddable.parent; + openReplacePanelFlyout({ + embeddable: dash, + core: this.core, + savedObjectFinder: this.savedobjectfinder, + notifications: this.notifications, + panelToRemove: view, + }); + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_flyout.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_flyout.tsx new file mode 100644 index 000000000000..0e738556372c --- /dev/null +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/replace_panel_flyout.tsx @@ -0,0 +1,146 @@ +/* + * 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 React from 'react'; + +import { NotificationsStart } from 'src/core/public'; +import { DashboardPanelState } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiGlobalToastListToast as Toast, +} from '@elastic/eui'; + +import { IContainer } from '../../../../../../embeddable_api/public/np_ready/public'; +import { + IEmbeddable, + EmbeddableInput, + EmbeddableOutput, +} from '../../../../../../embeddable_api/public/np_ready/public'; + +import { start } from '../../../../../../embeddable_api/public/np_ready/public/legacy'; + +interface Props { + container: IContainer; + savedObjectsFinder: React.ComponentType; + onClose: () => void; + notifications: NotificationsStart; + panelToRemove: IEmbeddable; +} + +export class ReplacePanelFlyout extends React.Component { + private lastToast: Toast = { + id: 'panelReplaceToast', + }; + + constructor(props: Props) { + super(props); + } + + public showToast = (name: string) => { + // To avoid the clutter of having toast messages cover flyout + // close previous toast message before creating a new one + if (this.lastToast) { + this.props.notifications.toasts.remove(this.lastToast); + } + + this.lastToast = this.props.notifications.toasts.addSuccess({ + title: i18n.translate( + 'dashboardEmbeddableContainer.addPanel.savedObjectAddedToContainerSuccessMessageTitle', + { + defaultMessage: '{savedObjectName} was added', + values: { + savedObjectName: name, + }, + } + ), + 'data-test-subj': 'addObjectToContainerSuccess', + }); + }; + + public onReplacePanel = async (id: string, type: string, name: string) => { + const originalPanels = this.props.container.getInput().panels; + const filteredPanels = { ...originalPanels }; + + const nnw = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.w; + const nnh = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.h; + const nnx = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.x; + const nny = (filteredPanels[this.props.panelToRemove.id] as DashboardPanelState).gridData.y; + + // add the new view + const newObj = await this.props.container.addSavedObjectEmbeddable(type, id); + + const finalPanels = this.props.container.getInput().panels; + (finalPanels[newObj.id] as DashboardPanelState).gridData.w = nnw; + (finalPanels[newObj.id] as DashboardPanelState).gridData.h = nnh; + (finalPanels[newObj.id] as DashboardPanelState).gridData.x = nnx; + (finalPanels[newObj.id] as DashboardPanelState).gridData.y = nny; + + // delete the old view + delete finalPanels[this.props.panelToRemove.id]; + + // apply changes + this.props.container.updateInput(finalPanels); + this.props.container.reload(); + + this.showToast(name); + this.props.onClose(); + }; + + public render() { + const SavedObjectFinder = this.props.savedObjectsFinder; + const savedObjectsFinder = ( + + Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType + ) + .map(({ savedObjectMetaData }) => savedObjectMetaData as any)} + showFilter={true} + onChoose={this.onReplacePanel} + /> + ); + + const panelToReplace = 'Replace panel ' + this.props.panelToRemove.getTitle() + ' with:'; + + return ( + + + +

+ {panelToReplace} +

+
+
+ {savedObjectsFinder} +
+ ); + } +} diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/plugin.ts index 3d243ab3aa37..bb18d109b0de 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/plugin.ts @@ -20,7 +20,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { IUiActionsSetup, IUiActionsStart } from '../../../../../../plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, Plugin as EmbeddablePlugin } from './lib/embeddable_api'; -import { ExpandPanelAction, DashboardContainerFactory } from './lib'; +import { ExpandPanelAction, ReplacePanelAction, DashboardContainerFactory } from './lib'; import { Start as InspectorStartContract } from '../../../../../../plugins/inspector/public'; interface SetupDependencies { @@ -55,6 +55,14 @@ export class DashboardEmbeddableContainerPublicPlugin const { application, notifications, overlays } = core; const { embeddable, inspector, __LEGACY, uiActions } = plugins; + const changeViewAction = new ReplacePanelAction( + core, + __LEGACY.SavedObjectFinder, + notifications + ); + uiActions.registerAction(changeViewAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction.id); + const factory = new DashboardContainerFactory({ application, notifications, diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index b67e1dd7b2eb..063ad1f79efa 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; -import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; +import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME, LINE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; import { VisualizeConstants } from '../../../../src/legacy/core_plugins/kibana/public/visualize/visualize_constants'; @@ -28,6 +28,8 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardReplacePanel = getService('dashboardReplacePanel'); + const dashboardVisualizations = getService('dashboardVisualizations'); const renderable = getService('renderable'); const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'discover']); const dashboardName = 'Dashboard Panel Controls Test'; @@ -44,6 +46,62 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardLandingPage(); }); + describe('visualization object replace flyout', () => { + let intialDimensions; + before(async () => { + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.setTimepickerInHistoricalDataRange(); + await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME); + await dashboardAddPanel.addVisualization(LINE_CHART_VIS_NAME); + intialDimensions = await PageObjects.dashboard.getPanelDimensions(); + }); + + after(async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('replaces old panel with selected panel', async () => { + await dashboardPanelActions.replacePanelByTitle(PIE_CHART_VIS_NAME); + await dashboardReplacePanel.replaceEmbeddable(AREA_CHART_VIS_NAME); + await PageObjects.header.waitUntilLoadingHasFinished(); + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + expect(panelTitles.length).to.be(2); + expect(panelTitles[0]).to.be(AREA_CHART_VIS_NAME); + }); + + it('replaces selected visualization with old dimensions', async () => { + const newDimensions = await PageObjects.dashboard.getPanelDimensions(); + expect(intialDimensions[0]).to.eql(newDimensions[0]); + }); + + it('replaced panel persisted correctly when dashboard is hard refreshed', async () => { + const currentUrl = await browser.getCurrentUrl(); + await browser.get(currentUrl, true); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + expect(panelTitles.length).to.be(2); + expect(panelTitles[0]).to.be(AREA_CHART_VIS_NAME); + }); + + it('replaced panel with saved search', async () => { + const replacedSearch = 'replaced saved search'; + await dashboardVisualizations.createSavedSearch({ name: replacedSearch, fields: ['bytes', 'agent'] }); + await PageObjects.header.clickDashboard(); + const inViewMode = await PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await PageObjects.dashboard.switchToEditMode(); + } + await dashboardPanelActions.replacePanelByTitle(AREA_CHART_VIS_NAME); + await dashboardReplacePanel.replaceEmbeddable(replacedSearch, 'search'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + expect(panelTitles.length).to.be(2); + expect(panelTitles[0]).to.be(replacedSearch); + }); + }); + describe('panel edit controls', function () { before(async () => { await PageObjects.dashboard.clickNewDashboard(); @@ -67,6 +125,7 @@ export default function ({ getService, getPageObjects }) { await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.expectExistsEditPanelAction(); + await dashboardPanelActions.expectExistsReplacePanelAction(); await dashboardPanelActions.expectExistsRemovePanelAction(); }); @@ -80,6 +139,7 @@ export default function ({ getService, getPageObjects }) { await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.expectExistsEditPanelAction(); + await dashboardPanelActions.expectExistsReplacePanelAction(); await dashboardPanelActions.expectExistsRemovePanelAction(); // Get rid of the timestamp in the url. @@ -94,6 +154,7 @@ export default function ({ getService, getPageObjects }) { await dashboardPanelActions.clickExpandPanelToggle(); await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.expectMissingEditPanelAction(); + await dashboardPanelActions.expectMissingReplacePanelAction(); await dashboardPanelActions.expectMissingRemovePanelAction(); }); @@ -101,6 +162,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.switchToEditMode(); await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.expectExistsEditPanelAction(); + await dashboardPanelActions.expectExistsReplacePanelAction(); await dashboardPanelActions.expectMissingRemovePanelAction(); await dashboardPanelActions.clickExpandPanelToggle(); }); @@ -126,13 +188,18 @@ export default function ({ getService, getPageObjects }) { }); describe('saved search object edit menu', () => { + const searchName = 'my search'; before(async () => { await PageObjects.header.clickDiscover(); - await PageObjects.discover.clickFieldListItemAdd('bytes'); - await PageObjects.discover.saveSearch('my search'); + await PageObjects.discover.clickNewSearchButton(); + await dashboardVisualizations.createSavedSearch({ name: searchName, fields: ['bytes'] }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.clickDashboard(); - await dashboardAddPanel.addSavedSearch('my search'); + const inViewMode = await PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await PageObjects.dashboard.switchToEditMode(); + } + await dashboardAddPanel.addSavedSearch(searchName); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(1); @@ -143,7 +210,7 @@ export default function ({ getService, getPageObjects }) { await dashboardPanelActions.clickEdit(); await PageObjects.header.waitUntilLoadingHasFinished(); const queryName = await PageObjects.discover.getCurrentQueryName(); - expect(queryName).to.be('my search'); + expect(queryName).to.be(searchName); }); it('deletes the saved search when delete link is clicked', async () => { diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 4f078d4b7a43..ca141114f976 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -22,6 +22,7 @@ import { DashboardConstants } from '../../../src/legacy/core_plugins/kibana/publ export const PIE_CHART_VIS_NAME = 'Visualization PieChart'; export const AREA_CHART_VIS_NAME = 'Visualization漢字 AreaChart'; +export const LINE_CHART_VIS_NAME = 'Visualization漢字 LineChart'; export function DashboardPageProvider({ getService, getPageObjects }) { const log = getService('log'); @@ -499,7 +500,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { { name: 'Visualization☺ VerticalBarChart', description: 'VerticalBarChart' }, { name: AREA_CHART_VIS_NAME, description: 'AreaChart' }, { name: 'Visualization☺漢字 DataTable', description: 'DataTable' }, - { name: 'Visualization漢字 LineChart', description: 'LineChart' }, + { name: LINE_CHART_VIS_NAME, description: 'LineChart' }, { name: 'Visualization TileMap', description: 'TileMap' }, { name: 'Visualization MetricChart', description: 'MetricChart' } ]; diff --git a/test/functional/services/dashboard/index.js b/test/functional/services/dashboard/index.js index b2de69015753..bb9b86168290 100644 --- a/test/functional/services/dashboard/index.js +++ b/test/functional/services/dashboard/index.js @@ -20,5 +20,6 @@ export { DashboardVisualizationProvider } from './visualizations'; export { DashboardExpectProvider } from './expectations'; export { DashboardAddPanelProvider } from './add_panel'; +export { DashboardReplacePanelProvider } from './replace_panel'; export { DashboardPanelActionsProvider } from './panel_actions'; diff --git a/test/functional/services/dashboard/panel_actions.js b/test/functional/services/dashboard/panel_actions.js index b7327f4af6d1..6826ea1ff171 100644 --- a/test/functional/services/dashboard/panel_actions.js +++ b/test/functional/services/dashboard/panel_actions.js @@ -19,6 +19,7 @@ const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel'; const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel'; +const REPLACE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-replacePanel'; const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-CUSTOMIZE_PANEL_ACTION_ID'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; @@ -87,6 +88,16 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { await testSubjects.click(CUSTOMIZE_PANEL_DATA_TEST_SUBJ); } + async replacePanelByTitle(title) { + log.debug(`replacePanel(${title})`); + let panelOptions = null; + if (title) { + panelOptions = await this.getPanelHeading(title); + } + await this.openContextMenu(panelOptions); + await testSubjects.click(REPLACE_PANEL_DATA_TEST_SUBJ); + } + async openInspectorByTitle(title) { const header = await this.getPanelHeading(title); await this.openInspector(header); @@ -112,11 +123,21 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { await testSubjects.existOrFail(EDIT_PANEL_DATA_TEST_SUBJ); } + async expectExistsReplacePanelAction() { + log.debug('expectExistsEditPanelAction'); + await testSubjects.existOrFail(REPLACE_PANEL_DATA_TEST_SUBJ); + } + async expectMissingEditPanelAction() { log.debug('expectMissingEditPanelAction'); await testSubjects.missingOrFail(EDIT_PANEL_DATA_TEST_SUBJ); } + async expectMissingReplacePanelAction() { + log.debug('expectMissingEditPanelAction'); + await testSubjects.missingOrFail(REPLACE_PANEL_DATA_TEST_SUBJ); + } + async expectExistsToggleExpandAction() { log.debug('expectExistsToggleExpandAction'); await testSubjects.existOrFail(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); diff --git a/test/functional/services/dashboard/replace_panel.js b/test/functional/services/dashboard/replace_panel.js new file mode 100644 index 000000000000..b3ea6f9cf21e --- /dev/null +++ b/test/functional/services/dashboard/replace_panel.js @@ -0,0 +1,102 @@ +/* + * 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 function DashboardReplacePanelProvider({ getService }) { + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const flyout = getService('flyout'); + + return new class DashboardReplacePanel { + async toggleFilterPopover() { + log.debug('DashboardReplacePanel.toggleFilter'); + await testSubjects.click('savedObjectFinderFilterButton'); + } + + async toggleFilter(type) { + log.debug(`DashboardReplacePanel.replaceToFilter(${type})`); + await this.waitForListLoading(); + await this.toggleFilterPopover(); + await testSubjects.click(`savedObjectFinderFilter-${type}`); + await this.toggleFilterPopover(); + } + + async isReplacePanelOpen() { + log.debug('DashboardReplacePanel.isReplacePanelOpen'); + return await testSubjects.exists('dashboardReplacePanel'); + } + + async ensureReplacePanelIsShowing() { + log.debug('DashboardReplacePanel.ensureReplacePanelIsShowing'); + const isOpen = await this.isReplacePanelOpen(); + if (!isOpen) { + throw new Error('Replace panel is not open, trying again.'); + } + } + + async waitForListLoading() { + await testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator'); + } + + async closeReplacePanel() { + await flyout.ensureClosed('dashboardReplacePanel'); + } + + async replaceSavedSearch(searchName) { + return this.replaceEmbeddable(searchName, 'search'); + } + + async replaceSavedSearches(searches) { + for (const name of searches) { + await this.replaceSavedSearch(name); + } + } + + async replaceVisualization(vizName) { + return this.replaceEmbeddable(vizName, 'visualization'); + } + + async replaceEmbeddable(embeddableName, embeddableType) { + log.debug(`DashboardReplacePanel.replaceEmbeddable, name: ${embeddableName}, type: ${embeddableType}`); + await this.ensureReplacePanelIsShowing(); + if (embeddableType) { + await this.toggleFilter(embeddableType); + } + await this.filterEmbeddableNames(`"${embeddableName.replace('-', ' ')}"`); + await testSubjects.click(`savedObjectTitle${embeddableName.split(' ').join('-')}`); + await testSubjects.exists('addObjectToDashboardSuccess'); + await this.closeReplacePanel(); + return embeddableName; + } + + async filterEmbeddableNames(name) { + // The search input field may be disabled while the table is loading so wait for it + await this.waitForListLoading(); + await testSubjects.setValue('savedObjectFinderSearchInput', name); + await this.waitForListLoading(); + } + + async panelReplaceLinkExists(name) { + log.debug(`DashboardReplacePanel.panelReplaceLinkExists(${name})`); + await this.ensureReplacePanelIsShowing(); + await this.filterEmbeddableNames(`"${name}"`); + return await testSubjects.exists(`savedObjectTitle${name.split(' ').join('-')}`); + } + }; +} diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 2566c3c87334..6098e9931f29 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -24,6 +24,7 @@ import { BrowserProvider } from './browser'; import { ComboBoxProvider } from './combo_box'; import { DashboardAddPanelProvider, + DashboardReplacePanelProvider, DashboardExpectProvider, DashboardPanelActionsProvider, DashboardVisualizationProvider, @@ -66,6 +67,7 @@ export const services = { failureDebugging: FailureDebuggingProvider, visualizeListingTable: VisualizeListingTableProvider, dashboardAddPanel: DashboardAddPanelProvider, + dashboardReplacePanel: DashboardReplacePanelProvider, dashboardPanelActions: DashboardPanelActionsProvider, flyout: FlyoutProvider, comboBox: ComboBoxProvider,