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:
parent
b23cfbdf5d
commit
c718972d11
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from 'kibana/public';
|
||||
import { PluginInitializerContext } from '../../../../../../core/public';
|
||||
import { DashboardEmbeddableContainerPublicPlugin } from './plugin';
|
||||
|
||||
export * from './lib';
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export { ExpandPanelAction, EXPAND_PANEL_ACTION } from './expand_panel_action';
|
||||
export { ReplacePanelAction, REPLACE_PANEL_ACTION } from './replace_panel_action';
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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' }
|
||||
];
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
102
test/functional/services/dashboard/replace_panel.js
Normal file
102
test/functional/services/dashboard/replace_panel.js
Normal 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('-')}`);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue