Add ability to replace any panel in edit mode on the fly (#45095)

* First version of change view functionality

* Adds the 'replace view' functionality to dashboard edit mode

* Fixed type_check errors

* Make action part of dashboard_embeddable_container

* Fixed import paths for type check errors

* Fixed i18n errors

* Renamed action to 'Replace panel' and adjusted jest tests to pass type check

* test: add functional tests

Closes #43900
This commit is contained in:
friol 2019-10-23 23:56:50 +02:00 committed by Nick Partridge
parent b23cfbdf5d
commit c718972d11
13 changed files with 636 additions and 8 deletions

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { PluginInitializerContext } from 'kibana/public';
import { PluginInitializerContext } from '../../../../../../core/public';
import { DashboardEmbeddableContainerPublicPlugin } from './plugin';
export * from './lib';

View file

@ -18,3 +18,4 @@
*/
export { ExpandPanelAction, EXPAND_PANEL_ACTION } from './expand_panel_action';
export { ReplacePanelAction, REPLACE_PANEL_ACTION } from './replace_panel_action';

View file

@ -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<any>;
notifications: NotificationsStart;
panelToRemove: IEmbeddable<EmbeddableInput, EmbeddableOutput>;
}) {
const { embeddable, core, panelToRemove, savedObjectFinder, notifications } = options;
const flyoutSession = core.overlays.openFlyout(
<ReplacePanelFlyout
container={embeddable}
onClose={() => {
if (flyoutSession) {
flyoutSession.close();
}
}}
panelToRemove={panelToRemove}
savedObjectsFinder={savedObjectFinder}
notifications={notifications}
/>,
{
'data-test-subj': 'replacePanelFlyout',
}
);
}

View file

@ -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<string, EmbeddableFactory>();
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<ContactCardEmbeddableInput>({
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();
});

View file

@ -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<ActionContext> {
public readonly type = REPLACE_PANEL_ACTION;
public readonly id = REPLACE_PANEL_ACTION;
public order = 11;
constructor(
private core: CoreStart,
private savedobjectfinder: React.ComponentType<any>,
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,
});
}
}

View file

@ -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<any>;
onClose: () => void;
notifications: NotificationsStart;
panelToRemove: IEmbeddable<EmbeddableInput, EmbeddableOutput>;
}
export class ReplacePanelFlyout extends React.Component<Props> {
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 = (
<SavedObjectFinder
noItemsMessage={i18n.translate(
'dashboardEmbeddableContainer.addPanel.noMatchingObjectsMessage',
{
defaultMessage: 'No matching objects found.',
}
)}
savedObjectMetaData={[...start.getEmbeddableFactories()]
.filter(
embeddableFactory =>
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 (
<EuiFlyout ownFocus onClose={this.props.onClose} data-test-subj="dashboardReplacePanel">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<span>{panelToReplace}</span>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>{savedObjectsFinder}</EuiFlyoutBody>
</EuiFlyout>
);
}
}

View file

@ -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,

View file

@ -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 () => {

View file

@ -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' }
];

View file

@ -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';

View file

@ -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);

View file

@ -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('-')}`);
}
};
}

View file

@ -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,