Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,19 @@ beforeEach(async () => {
});

test('Add to library is compatible when embeddable on dashboard has value type input', async () => {
const action = new AddToLibraryAction();
const action = new AddToLibraryAction(coreStart.notifications.toasts);
embeddable.updateInput(await embeddable.getInputAsValueType());
expect(await action.isCompatible({ embeddable })).toBe(true);
});

test('Add to library is not compatible when embeddable input is by reference', async () => {
const action = new AddToLibraryAction();
const action = new AddToLibraryAction(coreStart.notifications.toasts);
embeddable.updateInput(await embeddable.getInputAsRefType());
expect(await action.isCompatible({ embeddable })).toBe(false);
});

test('Add to library is not compatible when view mode is set to view', async () => {
const action = new AddToLibraryAction();
const action = new AddToLibraryAction(coreStart.notifications.toasts);
embeddable.updateInput(await embeddable.getInputAsRefType());
embeddable.updateInput({ viewMode: ViewMode.VIEW });
expect(await action.isCompatible({ embeddable })).toBe(false);
Expand All @@ -120,15 +120,15 @@ test('Add to library is not compatible when embeddable is not in a dashboard con
mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id },
mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id },
});
const action = new AddToLibraryAction();
const action = new AddToLibraryAction(coreStart.notifications.toasts);
expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false);
});

test('Add to library replaces embeddableId but retains panel count', async () => {
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelCount = Object.keys(dashboard.getInput().panels).length;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
const action = new AddToLibraryAction();
const action = new AddToLibraryAction(coreStart.notifications.toasts);
await action.execute({ embeddable });
expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount);

Expand All @@ -154,7 +154,7 @@ test('Add to library returns reference type input', async () => {
});
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
const action = new AddToLibraryAction();
const action = new AddToLibraryAction(coreStart.notifications.toasts);
await action.execute({ embeddable });
const newPanelId = Object.keys(container.getInput().panels).find(
(key) => !originalPanelKeySet.has(key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
EmbeddableInput,
isReferenceOrValueEmbeddable,
} from '../../../../embeddable/public';
import { NotificationsStart } from '../../../../../core/public';
import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..';

export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary';
Expand All @@ -40,7 +41,7 @@ export class AddToLibraryAction implements ActionByType<typeof ACTION_ADD_TO_LIB
public readonly id = ACTION_ADD_TO_LIBRARY;
public order = 15;

constructor() {}
constructor(private toasts: NotificationsStart['toasts']) {}

public getDisplayName({ embeddable }: AddToLibraryActionContext) {
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
Expand Down Expand Up @@ -89,5 +90,14 @@ export class AddToLibraryAction implements ActionByType<typeof ACTION_ADD_TO_LIB
explicitInput: { ...newInput, id: uuid.v4() },
};
dashboard.replacePanel(panelToReplace, newPanel);

const title = i18n.translate('dashboard.panel.addToLibrary.successMessage', {
defaultMessage: `Panel '{panelTitle}' was added to the visualize library`,
values: { panelTitle: embeddable.getTitle() },
});
this.toasts.addSuccess({
title,
'data-test-subj': 'unlinkPanelSuccess',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from '../../embeddable_plugin_test_samples';
import { coreMock } from '../../../../../core/public/mocks';
import { CoreStart } from 'kibana/public';
import { LibraryNotificationAction } from '.';
import { LibraryNotificationAction, UnlinkFromLibraryAction } from '.';
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
import { ViewMode } from '../../../../embeddable/public';

Expand All @@ -42,9 +42,16 @@ const start = doStart();
let container: DashboardContainer;
let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable;
let coreStart: CoreStart;
let unlinkAction: UnlinkFromLibraryAction;

beforeEach(async () => {
coreStart = coreMock.createStart();

unlinkAction = ({
getDisplayName: () => 'unlink from dat library',
execute: jest.fn(),
} as unknown) as UnlinkFromLibraryAction;

const containerOptions = {
ExitFullScreenButton: () => null,
SavedObjectFinder: () => null,
Expand Down Expand Up @@ -81,19 +88,19 @@ beforeEach(async () => {
});

test('Notification is shown when embeddable on dashboard has reference type input', async () => {
const action = new LibraryNotificationAction();
const action = new LibraryNotificationAction(unlinkAction);
embeddable.updateInput(await embeddable.getInputAsRefType());
expect(await action.isCompatible({ embeddable })).toBe(true);
});

test('Notification is not shown when embeddable input is by value', async () => {
const action = new LibraryNotificationAction();
const action = new LibraryNotificationAction(unlinkAction);
embeddable.updateInput(await embeddable.getInputAsValueType());
expect(await action.isCompatible({ embeddable })).toBe(false);
});

test('Notification is not shown when view mode is set to view', async () => {
const action = new LibraryNotificationAction();
const action = new LibraryNotificationAction(unlinkAction);
embeddable.updateInput(await embeddable.getInputAsRefType());
embeddable.updateInput({ viewMode: ViewMode.VIEW });
expect(await action.isCompatible({ embeddable })).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
* under the License.
*/

import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBadge } from '@elastic/eui';
import React from 'react';
import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin';
import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin';
import { reactToUiComponent } from '../../../../kibana_react/public';
import { UnlinkFromLibraryAction } from '.';
import { LibraryNotificationPopover } from './library_notification_popover';

export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION';

Expand All @@ -35,23 +36,32 @@ export class LibraryNotificationAction implements ActionByType<typeof ACTION_LIB
public readonly type = ACTION_LIBRARY_NOTIFICATION;
public readonly order = 1;

constructor(private unlinkAction: UnlinkFromLibraryAction) {}

private displayName = i18n.translate('dashboard.panel.LibraryNotification', {
defaultMessage: 'Library',
defaultMessage: 'Visualize Library',
});

private icon = 'folderCheck';

public readonly MenuItem = reactToUiComponent(() => (
<EuiBadge
data-test-subj={`embeddablePanelNotification-${this.id}`}
iconType={this.icon}
key={this.id}
style={{ marginTop: '2px', marginRight: '4px' }}
color="hollow"
>
{this.displayName}
</EuiBadge>
));
private LibraryNotification: React.FC<{ context: LibraryNotificationActionContext }> = ({
context,
}: {
context: LibraryNotificationActionContext;
}) => {
const { embeddable } = context;
return (
<LibraryNotificationPopover
unlinkAction={this.unlinkAction}
displayName={this.displayName}
context={context}
icon={this.getIconType({ embeddable })}
id={this.id}
/>
);
};

public readonly MenuItem = reactToUiComponent(this.LibraryNotification);

public getDisplayName({ embeddable }: LibraryNotificationActionContext) {
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
Expand All @@ -67,16 +77,6 @@ export class LibraryNotificationAction implements ActionByType<typeof ACTION_LIB
return this.icon;
}

public getDisplayNameTooltip = ({ embeddable }: LibraryNotificationActionContext) => {
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
throw new IncompatibleActionError();
}
return i18n.translate('dashboard.panel.libraryNotification.toolTip', {
defaultMessage:
'This panel is linked to a Library item. Editing the panel might affect other dashboards.',
});
};

public isCompatible = async ({ embeddable }: LibraryNotificationActionContext) => {
return (
embeddable.getRoot().isContainer &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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 { DashboardContainer } from '..';
import { isErrorEmbeddable } from '../../embeddable_plugin';
import { mountWithIntl } from '../../../../../test_utils/public/enzyme_helpers';
import { embeddablePluginMock } from '../../../../embeddable/public/mocks';
import { getSampleDashboardInput } from '../test_helpers';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableFactory,
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
} from '../../embeddable_plugin_test_samples';
import {
LibraryNotificationPopover,
LibraryNotificationProps,
} from './library_notification_popover';
import { CoreStart } from '../../../../../core/public';
import { coreMock } from '../../../../../core/public/mocks';
import { findTestSubject } from '@elastic/eui/lib/test';
import { EuiPopover } from '@elastic/eui';

describe('LibraryNotificationPopover', () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const start = doStart();

let container: DashboardContainer;
let defaultProps: LibraryNotificationProps;
let coreStart: CoreStart;

beforeEach(async () => {
coreStart = coreMock.createStart();

const containerOptions = {
ExitFullScreenButton: () => null,
SavedObjectFinder: () => null,
application: {} as any,
embeddable: start,
inspector: {} as any,
notifications: {} as any,
overlays: coreStart.overlays,
savedObjectMetaData: {} as any,
uiActions: {} as any,
};

container = new DashboardContainer(getSampleDashboardInput(), containerOptions);
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
firstName: 'Kibanana',
});

if (isErrorEmbeddable(contactCardEmbeddable)) {
throw new Error('Failed to create embeddable');
}

defaultProps = {
unlinkAction: ({
execute: jest.fn(),
getDisplayName: () => 'test unlink',
} as unknown) as LibraryNotificationProps['unlinkAction'],
displayName: 'test display',
context: { embeddable: contactCardEmbeddable },
icon: 'testIcon',
id: 'testId',
};
});

function mountComponent(props?: Partial<LibraryNotificationProps>) {
return mountWithIntl(<LibraryNotificationPopover {...{ ...defaultProps, ...props }} />);
}

test('click library notification badge should open and close popover', () => {
const component = mountComponent();
const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`);
btn.simulate('click');
let popover = component.find(EuiPopover);
expect(popover.prop('isOpen')).toBe(true);
btn.simulate('click');
popover = component.find(EuiPopover);
expect(popover.prop('isOpen')).toBe(false);
});

test('popover should contain button with unlink action display name', () => {
const component = mountComponent();
const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`);
btn.simulate('click');
const popover = component.find(EuiPopover);
const unlinkButton = findTestSubject(popover, 'libraryNotificationUnlinkButton');
expect(unlinkButton.text()).toEqual('test unlink');
});

test('clicking unlink executes unlink action', () => {
const component = mountComponent();
const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`);
btn.simulate('click');
const popover = component.find(EuiPopover);
const unlinkButton = findTestSubject(popover, 'libraryNotificationUnlinkButton');
unlinkButton.simulate('click');
expect(defaultProps.unlinkAction.execute).toHaveBeenCalled();
});
});
Loading