[Time to Visualize] Copy Panel to Dashboard (#90856) (#91228)

* Added copy to dashboard action
This commit is contained in:
Devon Thomson 2021-02-11 19:26:02 -05:00 committed by GitHub
parent ac8f5bc23b
commit 4396e45c4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 494 additions and 16 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) &gt; [maxWidth](./kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md)
## OverlayModalOpenOptions.maxWidth property
<b>Signature:</b>
```typescript
maxWidth?: boolean | number | string;
```

View file

@ -18,4 +18,5 @@ export interface OverlayModalOpenOptions
| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) | <code>string</code> | |
| [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) | <code>string</code> | |
| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) | <code>string</code> | |
| [maxWidth](./kibana-plugin-core-public.overlaymodalopenoptions.maxwidth.md) | <code>boolean &#124; number &#124; string</code> | |

View file

@ -101,6 +101,7 @@ export interface OverlayModalOpenOptions {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
maxWidth?: boolean | number | string;
}
interface StartDeps {

View file

@ -976,6 +976,8 @@ export interface OverlayModalOpenOptions {
className?: string;
// (undocumented)
closeButtonAriaLabel?: string;
// (undocumented)
maxWidth?: boolean | number | string;
}
// @public

View file

@ -10,7 +10,8 @@
"savedObjects",
"share",
"uiActions",
"urlForwarding"
"urlForwarding",
"presentationUtil"
],
"optionalPlugins": [
"home",

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { OverlayStart } from '../../../../../core/public';
import { dashboardCopyToDashboardAction } from '../../dashboard_strings';
import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable';
import { toMountPoint } from '../../services/kibana_react';
import { PresentationUtilPluginStart } from '../../services/presentation_util';
import { Action, IncompatibleActionError } from '../../services/ui_actions';
import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable';
import { CopyToDashboardModal } from './copy_to_dashboard_modal';
export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard';
export interface CopyToDashboardActionContext {
embeddable: IEmbeddable;
}
export interface DashboardCopyToCapabilities {
canCreateNew: boolean;
canEditExisting: boolean;
}
function isDashboard(embeddable: IEmbeddable): embeddable is DashboardContainer {
return embeddable.type === DASHBOARD_CONTAINER_TYPE;
}
export class CopyToDashboardAction implements Action<CopyToDashboardActionContext> {
public readonly type = ACTION_COPY_TO_DASHBOARD;
public readonly id = ACTION_COPY_TO_DASHBOARD;
public order = 1;
constructor(
private overlays: OverlayStart,
private stateTransfer: EmbeddableStateTransfer,
private capabilities: DashboardCopyToCapabilities,
private PresentationUtilContext: PresentationUtilPluginStart['ContextProvider']
) {}
public getDisplayName({ embeddable }: CopyToDashboardActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
return dashboardCopyToDashboardAction.getDisplayName();
}
public getIconType({ embeddable }: CopyToDashboardActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
return 'exit';
}
public async isCompatible({ embeddable }: CopyToDashboardActionContext) {
return Boolean(
embeddable.parent &&
isDashboard(embeddable.parent) &&
(this.capabilities.canCreateNew || this.capabilities.canEditExisting)
);
}
public async execute({ embeddable }: CopyToDashboardActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
const session = this.overlays.openModal(
toMountPoint(
<CopyToDashboardModal
PresentationUtilContext={this.PresentationUtilContext}
closeModal={() => session.close()}
stateTransfer={this.stateTransfer}
capabilities={this.capabilities}
dashboardId={(embeddable.parent as DashboardContainer).getInput().id}
embeddable={embeddable}
/>
),
{
maxWidth: 400,
'data-test-subj': 'copyToDashboardPanel',
}
);
}
}

View file

@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback, useState } from 'react';
import { omit } from 'lodash';
import {
EuiButton,
EuiButtonEmpty,
EuiFormRow,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiPanel,
EuiRadio,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { DashboardCopyToCapabilities } from './copy_to_dashboard_action';
import { DashboardPicker } from '../../services/presentation_util';
import { dashboardCopyToDashboardAction } from '../../dashboard_strings';
import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable';
import { createDashboardEditUrl, DashboardConstants } from '../..';
interface CopyToDashboardModalProps {
capabilities: DashboardCopyToCapabilities;
stateTransfer: EmbeddableStateTransfer;
PresentationUtilContext: React.FC;
embeddable: IEmbeddable;
dashboardId?: string;
closeModal: () => void;
}
export function CopyToDashboardModal({
PresentationUtilContext,
stateTransfer,
capabilities,
dashboardId,
embeddable,
closeModal,
}: CopyToDashboardModalProps) {
const [dashboardOption, setDashboardOption] = useState<'new' | 'existing'>('existing');
const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>(
null
);
const onSubmit = useCallback(() => {
const state = {
input: omit(embeddable.getInput(), 'id'),
type: embeddable.type,
};
const path =
dashboardOption === 'existing' && selectedDashboard
? `#${createDashboardEditUrl(selectedDashboard.id, true)}`
: `#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`;
closeModal();
stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
state,
path,
});
}, [dashboardOption, embeddable, selectedDashboard, stateTransfer, closeModal]);
return (
<PresentationUtilContext>
<EuiModalHeader>
<EuiModalHeaderTitle>{dashboardCopyToDashboardAction.getDisplayName()}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<>
<EuiText>
<p>{dashboardCopyToDashboardAction.getDescription()}</p>
</EuiText>
<EuiSpacer />
<EuiFormRow hasChildLabel={false}>
<EuiPanel color="subdued" hasShadow={false} data-test-subj="add-to-dashboard-options">
<div>
{capabilities.canEditExisting && (
<>
<EuiRadio
checked={dashboardOption === 'existing'}
data-test-subj="add-to-existing-dashboard-option"
id="existing-dashboard-option"
name="dashboard-option"
label={dashboardCopyToDashboardAction.getExistingDashboardOption()}
onChange={() => setDashboardOption('existing')}
/>
<div className="savAddDashboard__searchDashboards">
<DashboardPicker
isDisabled={dashboardOption !== 'existing'}
idsToOmit={dashboardId ? [dashboardId] : undefined}
onChange={(dashboard) => setSelectedDashboard(dashboard)}
/>
</div>
<EuiSpacer size="s" />
</>
)}
{capabilities.canCreateNew && (
<>
<EuiRadio
checked={dashboardOption === 'new'}
data-test-subj="add-to-new-dashboard-option"
id="new-dashboard-option"
name="dashboard-option"
disabled={!dashboardId}
label={dashboardCopyToDashboardAction.getNewDashboardOption()}
onChange={() => setDashboardOption('new')}
/>
<EuiSpacer size="s" />
</>
)}
</div>
</EuiPanel>
</EuiFormRow>
</>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty data-test-subj="cancelCopyToButton" onClick={() => closeModal()}>
{dashboardCopyToDashboardAction.getCancelButtonName()}
</EuiButtonEmpty>
<EuiButton
fill
data-test-subj="confirmCopyToButton"
onClick={onSubmit}
disabled={dashboardOption === 'existing' && !selectedDashboard}
>
{dashboardCopyToDashboardAction.getAcceptButtonName()}
</EuiButton>
</EuiModalFooter>
</PresentationUtilContext>
);
}

View file

@ -31,6 +31,11 @@ export {
UnlinkFromLibraryActionContext,
ACTION_UNLINK_FROM_LIBRARY,
} from './unlink_from_library_action';
export {
CopyToDashboardAction,
CopyToDashboardActionContext,
ACTION_COPY_TO_DASHBOARD,
} from './copy_to_dashboard_action';
export {
LibraryNotificationActionContext,
LibraryNotificationAction,

View file

@ -9,7 +9,6 @@
import {
EuiButton,
EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
@ -48,7 +47,7 @@ export const confirmCreateWithUnsaved = (
) => {
const session = overlays.openModal(
toMountPoint(
<EuiModal onClose={() => session.close()}>
<>
<EuiModalHeader data-test-subj="dashboardCreateConfirm">
<EuiModalHeaderTitle>{createConfirmStrings.getCreateTitle()}</EuiModalHeaderTitle>
</EuiModalHeader>
@ -85,7 +84,7 @@ export const confirmCreateWithUnsaved = (
{createConfirmStrings.getContinueButtonText()}
</EuiButton>
</EuiModalFooter>
</EuiModal>
</>
),
{
'data-test-subj': 'dashboardCreateConfirmModal',

View file

@ -75,6 +75,33 @@ export const dashboardFeatureCatalog = {
/*
Actions
*/
export const dashboardCopyToDashboardAction = {
getDisplayName: () =>
i18n.translate('dashboard.panel.copyToDashboard.title', {
defaultMessage: 'Copy to dashboard',
}),
getCancelButtonName: () =>
i18n.translate('dashboard.panel.copyToDashboard.cancel', {
defaultMessage: 'Cancel',
}),
getAcceptButtonName: () =>
i18n.translate('dashboard.panel.copyToDashboard.goToDashboard', {
defaultMessage: 'Copy and go to dashboard',
}),
getNewDashboardOption: () =>
i18n.translate('dashboard.panel.copyToDashboard.newDashboardOptionLabel', {
defaultMessage: 'New dashboard',
}),
getExistingDashboardOption: () =>
i18n.translate('dashboard.panel.copyToDashboard.existingDashboardOptionLabel', {
defaultMessage: 'Existing dashboard',
}),
getDescription: () =>
i18n.translate('dashboard.panel.copyToDashboard.description', {
defaultMessage: "Select where to copy the panel. You're navigated to destination dashboard.",
}),
};
export const dashboardAddToLibraryAction = {
getDisplayName: () =>
i18n.translate('dashboard.panel.AddToLibrary', {

View file

@ -28,6 +28,7 @@ import {
import { createKbnUrlTracker } from './services/kibana_utils';
import { UsageCollectionSetup } from './services/usage_collection';
import { UiActionsSetup, UiActionsStart } from './services/ui_actions';
import { PresentationUtilPluginStart } from './services/presentation_util';
import { KibanaLegacySetup, KibanaLegacyStart } from './services/kibana_legacy';
import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home';
import { NavigationPublicPluginStart as NavigationStart } from './services/navigation';
@ -61,6 +62,7 @@ import {
UnlinkFromLibraryAction,
AddToLibraryAction,
LibraryNotificationAction,
CopyToDashboardAction,
} from './application';
import {
createDashboardUrlGenerator,
@ -109,6 +111,7 @@ export interface DashboardStartDependencies {
share?: SharePluginStart;
uiActions: UiActionsStart;
savedObjects: SavedObjectsStart;
presentationUtil: PresentationUtilPluginStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
}
@ -337,8 +340,8 @@ export class DashboardPlugin
}
public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart {
const { notifications } = core;
const { uiActions, data, share } = plugins;
const { notifications, overlays } = core;
const { uiActions, data, share, presentationUtil, embeddable } = plugins;
const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings);
@ -376,6 +379,18 @@ export class DashboardPlugin
const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction);
uiActions.registerAction(libraryNotificationAction);
uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id);
const copyToDashboardAction = new CopyToDashboardAction(
overlays,
embeddable.getStateTransfer(),
{
canCreateNew: Boolean(core.application.capabilities.dashboard.createNew),
canEditExisting: !Boolean(core.application.capabilities.dashboard.hideWriteControls),
},
presentationUtil.ContextProvider
);
uiActions.registerAction(copyToDashboardAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, copyToDashboardAction.id);
}
const savedDashboardLoader = createSavedDashboardLoader({

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { PresentationUtilPluginStart, DashboardPicker } from '../../../presentation_util/public';

View file

@ -21,6 +21,7 @@
{ "path": "../kibana_react/tsconfig.json" },
{ "path": "../kibana_utils/tsconfig.json" },
{ "path": "../share/tsconfig.json" },
{ "path": "../presentation_util/tsconfig.json" },
{ "path": "../url_forwarding/tsconfig.json" },
{ "path": "../usage_collection/tsconfig.json" },
{ "path": "../data/tsconfig.json"},

View file

@ -4,6 +4,6 @@
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"requiredPlugins": ["dashboard", "savedObjects"],
"requiredPlugins": ["savedObjects"],
"optionalPlugins": []
}

View file

@ -16,6 +16,7 @@ import { pluginServices } from '../services';
export interface DashboardPickerProps {
onChange: (dashboard: { name: string; id: string } | null) => void;
isDisabled: boolean;
idsToOmit?: string[];
}
interface DashboardOption {
@ -49,7 +50,18 @@ export function DashboardPicker(props: DashboardPickerProps) {
}
if (objects) {
setDashboardOptions(objects.map((d) => ({ value: d.id, label: d.attributes.title })));
setDashboardOptions(
objects
.filter((d) => !props.idsToOmit || !props.idsToOmit.includes(d.id))
.map((d) => ({
value: d.id,
label: d.attributes.title,
'data-test-subj': `dashboard-picker-option-${d.attributes.title.replaceAll(
' ',
'-'
)}`,
}))
);
}
setIsLoadingDashboards(false);
@ -60,7 +72,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
return () => {
cleanedUp = true;
};
}, [findDashboardsByTitle, query]);
}, [findDashboardsByTitle, query, props.idsToOmit]);
return (
<EuiComboBox

View file

@ -7,14 +7,17 @@
*/
import { SimpleSavedObject } from 'src/core/public';
import { DashboardSavedObject } from 'src/plugins/dashboard/public';
import { PluginServices } from './create';
import { PartialDashboardAttributes } from './kibana/dashboards';
export interface PresentationDashboardsService {
findDashboards: (
query: string,
fields: string[]
) => Promise<Array<SimpleSavedObject<DashboardSavedObject>>>;
findDashboardsByTitle: (title: string) => Promise<Array<SimpleSavedObject<DashboardSavedObject>>>;
) => Promise<Array<SimpleSavedObject<PartialDashboardAttributes>>>;
findDashboardsByTitle: (
title: string
) => Promise<Array<SimpleSavedObject<PartialDashboardAttributes>>>;
}
export interface PresentationCapabilitiesService {

View file

@ -6,8 +6,6 @@
* Side Public License, v 1.
*/
import { DashboardSavedObject } from 'src/plugins/dashboard/public';
import { PresentationUtilPluginStartDeps } from '../../types';
import { KibanaPluginServiceFactory } from '../create';
import { PresentationDashboardsService } from '..';
@ -17,11 +15,15 @@ export type DashboardsServiceFactory = KibanaPluginServiceFactory<
PresentationUtilPluginStartDeps
>;
export interface PartialDashboardAttributes {
title: string;
}
export const dashboardsServiceFactory: DashboardsServiceFactory = ({ coreStart }) => {
const findDashboards = async (query: string = '', fields: string[] = []) => {
const { find } = coreStart.savedObjects.client;
const { savedObjects } = await find<DashboardSavedObject>({
const { savedObjects } = await find<PartialDashboardAttributes>({
type: 'dashboard',
search: `${query}*`,
searchFields: fields,

View file

@ -10,7 +10,6 @@
"include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../dashboard/tsconfig.json" },
{ "path": "../saved_objects/tsconfig.json" },
]
}

View file

@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardVisualizations = getService('dashboardVisualizations');
const dashboardPanelActions = getService('dashboardPanelActions');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const find = getService('find');
const PageObjects = getPageObjects([
'header',
'common',
'discover',
'dashboard',
'visualize',
'timePicker',
]);
const fewPanelsTitle = 'few panels';
const markdownTitle = 'Copy To Markdown';
let fewPanelsPanelCount = 0;
const openCopyToModal = async (panelName: string) => {
await dashboardPanelActions.openCopyToModalByTitle(panelName);
const modalIsOpened = await testSubjects.exists('copyToDashboardPanel');
expect(modalIsOpened).to.be(true);
const hasDashboardSelector = await testSubjects.exists('add-to-dashboard-options');
expect(hasDashboardSelector).to.be(true);
};
describe('dashboard panel copy to', function viewEditModeTests() {
before(async function () {
await esArchiver.load('dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
});
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.preserveCrossAppState();
await PageObjects.dashboard.loadSavedDashboard(fewPanelsTitle);
await PageObjects.dashboard.waitForRenderComplete();
fewPanelsPanelCount = await PageObjects.dashboard.getPanelCount();
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.timePicker.setHistoricalDataRange();
await dashboardVisualizations.createAndAddMarkdown({
name: markdownTitle,
markdown: 'Please add me to some other dashboard',
});
});
after(async function () {
await PageObjects.dashboard.gotoDashboardLandingPage();
});
it('does not show the new dashboard option when on a new dashboard', async () => {
await openCopyToModal(markdownTitle);
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
const isDisabled = await dashboardSelector.findByCssSelector(
`input[id="new-dashboard-option"]:disabled`
);
expect(isDisabled).not.to.be(null);
await testSubjects.click('cancelCopyToButton');
});
it('copies a panel to an existing dashboard', async () => {
await openCopyToModal(markdownTitle);
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
const label = await dashboardSelector.findByCssSelector(
`label[for="existing-dashboard-option"]`
);
await label.click();
await testSubjects.setValue('dashboardPickerInput', fewPanelsTitle);
await testSubjects.existOrFail(`dashboard-picker-option-few-panels`);
await find.clickByButtonText(fewPanelsTitle);
await testSubjects.click('confirmCopyToButton');
await PageObjects.dashboard.waitForRenderComplete();
await PageObjects.dashboard.expectOnDashboard(`Editing ${fewPanelsTitle}`);
const newPanelCount = await PageObjects.dashboard.getPanelCount();
expect(newPanelCount).to.be(fewPanelsPanelCount + 1);
});
it('does not show the current dashboard in the dashboard picker', async () => {
await openCopyToModal(markdownTitle);
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
const label = await dashboardSelector.findByCssSelector(
`label[for="existing-dashboard-option"]`
);
await label.click();
await testSubjects.setValue('dashboardPickerInput', fewPanelsTitle);
await testSubjects.missingOrFail(`dashboard-picker-option-few-panels`);
await testSubjects.click('cancelCopyToButton');
});
it('copies a panel to a new dashboard', async () => {
await openCopyToModal(markdownTitle);
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
const label = await dashboardSelector.findByCssSelector(`label[for="new-dashboard-option"]`);
await label.click();
await testSubjects.click('confirmCopyToButton');
await PageObjects.dashboard.waitForRenderComplete();
await PageObjects.dashboard.expectOnDashboard(`Editing New Dashboard (unsaved)`);
});
it('it always appends new panels instead of overwriting', async () => {
const newPanelCount = await PageObjects.dashboard.getPanelCount();
expect(newPanelCount).to.be(2);
});
});
}

View file

@ -96,6 +96,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./bwc_shared_urls'));
loadTestFile(require.resolve('./panel_replacing'));
loadTestFile(require.resolve('./panel_cloning'));
loadTestFile(require.resolve('./copy_panel_to'));
loadTestFile(require.resolve('./panel_context_menu'));
loadTestFile(require.resolve('./dashboard_state'));
});

View file

@ -16,6 +16,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide
const find = getService('find');
const retry = getService('retry');
const browser = getService('browser');
const globalNav = getService('globalNav');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
@ -157,6 +158,13 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide
await testSubjects.click('breadcrumb dashboardListingBreadcrumb first');
}
public async expectOnDashboard(dashboardTitle: string) {
await retry.waitFor(
'last breadcrumb to have dashboard title',
async () => (await globalNav.getLastBreadcrumb()) === dashboardTitle
);
}
public async gotoDashboardLandingPage(ignorePageLeaveWarning = true) {
log.debug('gotoDashboardLandingPage');
const onPage = await this.onDashboardLandingPage();

View file

@ -17,6 +17,7 @@ const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel';
const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL';
const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon';
const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector';
const COPY_PANEL_TO_DATA_TEST_SUBJ = 'embeddablePanelAction-copyToDashboard';
const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION';
const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary';
@ -148,6 +149,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft
await testSubjects.click(CLONE_PANEL_DATA_TEST_SUBJ);
}
async openCopyToModalByTitle(title?: string) {
log.debug(`copyPanelTo(${title})`);
if (title) {
const panelOptions = await this.getPanelHeading(title);
await this.openContextMenu(panelOptions);
} else {
await this.openContextMenu();
}
const isActionVisible = await testSubjects.exists(COPY_PANEL_TO_DATA_TEST_SUBJ);
if (!isActionVisible) await this.clickContextMenuMoreItem();
await testSubjects.click(COPY_PANEL_TO_DATA_TEST_SUBJ);
}
async openInspectorByTitle(title: string) {
const header = await this.getPanelHeading(title);
await this.openInspector(header);

View file

@ -20,6 +20,10 @@ export default function ({ getService, getPageObjects }) {
it('allows to register links into the context menu', async () => {
await dashboardPanelActions.openContextMenu();
const actionExists = await testSubjects.exists('embeddablePanelAction-samplePanelLink');
if (!actionExists) {
await dashboardPanelActions.clickContextMenuMoreItem();
}
const actionElement = await testSubjects.find('embeddablePanelAction-samplePanelLink');
const actionElementTag = await actionElement.getTagName();
expect(actionElementTag).to.be('a');

View file

@ -62,6 +62,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-EcommerceData');
await dashboardPanelActions.toggleContextMenu(savedSearchPanel);
const actionExists = await testSubjects.exists('embeddablePanelAction-downloadCsvReport');
if (!actionExists) {
await dashboardPanelActions.clickContextMenuMoreItem();
}
await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport'); // wait for the full panel to display or else the test runner could click the wrong option!
await testSubjects.click('embeddablePanelAction-downloadCsvReport');
await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel
@ -80,6 +84,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
); // panel title is hidden
await dashboardPanelActions.toggleContextMenu(savedSearchPanel);
const actionExists = await testSubjects.exists('embeddablePanelAction-downloadCsvReport');
if (!actionExists) {
await dashboardPanelActions.clickContextMenuMoreItem();
}
await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport');
await testSubjects.click('embeddablePanelAction-downloadCsvReport');
await testSubjects.existOrFail('csvDownloadStarted');