From 249bc8cb52c6a86336ce7a30bd61daccdbcc9847 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 1 Sep 2020 16:23:56 -0400 Subject: [PATCH] [Dashboard First] Library Notification (#76122) (#76420) Added a notification to show when an embeddable is saved to the Visualize Library --- .../public/application/actions/index.ts | 5 + .../library_notification_action.test.tsx | 100 ++++++++++++++++++ .../actions/library_notification_action.tsx | 89 ++++++++++++++++ src/plugins/dashboard/public/plugin.tsx | 17 ++- .../lib/panel/panel_header/panel_header.tsx | 5 +- 5 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx create mode 100644 src/plugins/dashboard/public/application/actions/library_notification_action.tsx diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index 4343a3409b696..cd32c2025456f 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -42,3 +42,8 @@ export { UnlinkFromLibraryActionContext, ACTION_UNLINK_FROM_LIBRARY, } from './unlink_from_library_action'; +export { + LibraryNotificationActionContext, + LibraryNotificationAction, + ACTION_LIBRARY_NOTIFICATION, +} from './library_notification_action'; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx new file mode 100644 index 0000000000000..385f6f14ba94c --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -0,0 +1,100 @@ +/* + * 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, ReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { DashboardContainer } from '../embeddable'; +import { getSampleDashboardInput } from '../test_helpers'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from '../../embeddable_plugin_test_samples'; +import { coreMock } from '../../../../../core/public/mocks'; +import { CoreStart } from 'kibana/public'; +import { LibraryNotificationAction } from '.'; +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; +import { ViewMode } from '../../../../embeddable/public'; + +const { setup, doStart } = embeddablePluginMock.createInstance(); +setup.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory((() => null) as any, {} as any) +); +const start = doStart(); + +let container: DashboardContainer; +let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable; +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'); + } + embeddable = embeddablePluginMock.mockRefOrValEmbeddable< + ContactCardEmbeddable, + ContactCardEmbeddableInput + >(contactCardEmbeddable, { + mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: contactCardEmbeddable.id }, + mockedByValueInput: { firstName: 'Kibanana', id: contactCardEmbeddable.id }, + }); + embeddable.updateInput({ viewMode: ViewMode.EDIT }); +}); + +test('Notification is shown when embeddable on dashboard has reference type input', async () => { + const action = new LibraryNotificationAction(); + 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(); + 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(); + embeddable.updateInput(await embeddable.getInputAsRefType()); + embeddable.updateInput({ viewMode: ViewMode.VIEW }); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx new file mode 100644 index 0000000000000..974b55275ccc1 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -0,0 +1,89 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; +import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; +import { reactToUiComponent } from '../../../../kibana_react/public'; + +export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION'; + +export interface LibraryNotificationActionContext { + embeddable: IEmbeddable; +} + +export class LibraryNotificationAction implements ActionByType { + public readonly id = ACTION_LIBRARY_NOTIFICATION; + public readonly type = ACTION_LIBRARY_NOTIFICATION; + public readonly order = 1; + + private displayName = i18n.translate('dashboard.panel.LibraryNotification', { + defaultMessage: 'Library', + }); + + private icon = 'folderCheck'; + + public readonly MenuItem = reactToUiComponent(() => ( + + {this.displayName} + + )); + + public getDisplayName({ embeddable }: LibraryNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return this.displayName; + } + + public getIconType({ embeddable }: LibraryNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + 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.getInput()?.viewMode !== ViewMode.VIEW && + isReferenceOrValueEmbeddable(embeddable) && + embeddable.inputIsRefType(embeddable.getInput()) + ); + }; + + public execute = async () => {}; +} diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index b12bd0d31800b..836b3a3979958 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -40,6 +40,7 @@ import { EmbeddableStart, SavedObjectEmbeddableInput, EmbeddableInput, + PANEL_NOTIFICATION_TRIGGER, } from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; @@ -83,6 +84,12 @@ import { ACTION_UNLINK_FROM_LIBRARY, UnlinkFromLibraryActionContext, UnlinkFromLibraryAction, + ACTION_ADD_TO_LIBRARY, + AddToLibraryActionContext, + AddToLibraryAction, + ACTION_LIBRARY_NOTIFICATION, + LibraryNotificationActionContext, + LibraryNotificationAction, } from './application'; import { createDashboardUrlGenerator, @@ -95,11 +102,6 @@ import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; import { AttributeService } from '.'; -import { - AddToLibraryAction, - ACTION_ADD_TO_LIBRARY, - AddToLibraryActionContext, -} from './application/actions/add_to_library_action'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -162,6 +164,7 @@ declare module '../../../plugins/ui_actions/public' { [ACTION_CLONE_PANEL]: ClonePanelActionContext; [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; + [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; } } @@ -437,6 +440,10 @@ export class DashboardPlugin const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); + + const libraryNotificationAction = new LibraryNotificationAction(); + uiActions.registerAction(libraryNotificationAction); + uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); } const savedDashboardLoader = createSavedDashboardLoader({ diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 5d7daaa7217ed..f3c4cae720193 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -31,6 +31,7 @@ import { Action } from 'src/plugins/ui_actions/public'; import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers'; +import { uiToReactComponent } from '../../../../../kibana_react/public'; export interface PanelHeaderProps { title?: string; @@ -65,7 +66,9 @@ function renderNotifications( return notifications.map((notification) => { const context = { embeddable }; - let badge = ( + let badge = notification.MenuItem ? ( + React.createElement(uiToReactComponent(notification.MenuItem)) + ) : (