diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b8b1bdcdee3b..6a90fd49f1d6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -82,6 +82,11 @@ The plugin integrates with the core system via lifecycle events: `setup` | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | | | [OverlayBannersStart](./kibana-plugin-core-public.overlaybannersstart.md) | | +| [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) | | +| [OverlayFlyoutStart](./kibana-plugin-core-public.overlayflyoutstart.md) | APIs to open and manage fly-out dialogs. | +| [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) | | +| [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) | | +| [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) | APIs to open and manage modal dialogs. | | [OverlayRef](./kibana-plugin-core-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-core-public.overlaystart.md) methods for closing a mounted overlay. | | [OverlayStart](./kibana-plugin-core-public.overlaystart.md) | | | [Plugin](./kibana-plugin-core-public.plugin.md) | The interface that should be returned by a PluginInitializer. | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md new file mode 100644 index 000000000000..d583aae0e0b1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) + +## OverlayFlyoutOpenOptions."data-test-subj" property + +Signature: + +```typescript +'data-test-subj'?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.classname.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.classname.md new file mode 100644 index 000000000000..26f6db77ccce --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.classname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) + +## OverlayFlyoutOpenOptions.className property + +Signature: + +```typescript +className?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md new file mode 100644 index 000000000000..44014b7f0d81 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) + +## OverlayFlyoutOpenOptions.closeButtonAriaLabel property + +Signature: + +```typescript +closeButtonAriaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md new file mode 100644 index 000000000000..5945bca01f55 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) + +## OverlayFlyoutOpenOptions interface + + +Signature: + +```typescript +export interface OverlayFlyoutOpenOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | string | | +| [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | string | | +| [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | string | | +| [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | boolean | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md new file mode 100644 index 000000000000..337ce2c48e1d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) + +## OverlayFlyoutOpenOptions.ownFocus property + +Signature: + +```typescript +ownFocus?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.md new file mode 100644 index 000000000000..790fd5732041 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutStart](./kibana-plugin-core-public.overlayflyoutstart.md) + +## OverlayFlyoutStart interface + +APIs to open and manage fly-out dialogs. + +Signature: + +```typescript +export interface OverlayFlyoutStart +``` + +## Methods + +| Method | Description | +| --- | --- | +| [open(mount, options)](./kibana-plugin-core-public.overlayflyoutstart.open.md) | Opens a flyout panel with the given mount point inside. You can use close() on the returned FlyoutRef to close the flyout. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md new file mode 100644 index 000000000000..1f740410ca28 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutStart](./kibana-plugin-core-public.overlayflyoutstart.md) > [open](./kibana-plugin-core-public.overlayflyoutstart.open.md) + +## OverlayFlyoutStart.open() method + +Opens a flyout panel with the given mount point inside. You can use `close()` on the returned FlyoutRef to close the flyout. + +Signature: + +```typescript +open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mount | MountPoint | | +| options | OverlayFlyoutOpenOptions | | + +Returns: + +`OverlayRef` + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md new file mode 100644 index 000000000000..3569b2153c3d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > ["data-test-subj"](./kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md) + +## OverlayModalConfirmOptions."data-test-subj" property + +Signature: + +```typescript +'data-test-subj'?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md new file mode 100644 index 000000000000..5c827e19e42e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [buttonColor](./kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md) + +## OverlayModalConfirmOptions.buttonColor property + +Signature: + +```typescript +buttonColor?: EuiConfirmModalProps['buttonColor']; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md new file mode 100644 index 000000000000..0c0b9fd48dbd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [cancelButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md) + +## OverlayModalConfirmOptions.cancelButtonText property + +Signature: + +```typescript +cancelButtonText?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md new file mode 100644 index 000000000000..0a622aeaac41 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [className](./kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md) + +## OverlayModalConfirmOptions.className property + +Signature: + +```typescript +className?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md new file mode 100644 index 000000000000..8a321a0b07b4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md) + +## OverlayModalConfirmOptions.closeButtonAriaLabel property + +Signature: + +```typescript +closeButtonAriaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md new file mode 100644 index 000000000000..f84d834369f5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [confirmButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md) + +## OverlayModalConfirmOptions.confirmButtonText property + +Signature: + +```typescript +confirmButtonText?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md new file mode 100644 index 000000000000..c5edf48b54ea --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [defaultFocusedButton](./kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md) + +## OverlayModalConfirmOptions.defaultFocusedButton property + +Signature: + +```typescript +defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md new file mode 100644 index 000000000000..488b4eb3794f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [maxWidth](./kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md) + +## OverlayModalConfirmOptions.maxWidth property + +Sets the max-width of the modal. Set to `true` to use the default (`euiBreakpoints 'm'`), set to `false` to not restrict the width, set to a number for a custom width in px, set to a string for a custom width in custom measurement. + +Signature: + +```typescript +maxWidth?: boolean | number | string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md new file mode 100644 index 000000000000..83405a151a37 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) + +## OverlayModalConfirmOptions interface + + +Signature: + +```typescript +export interface OverlayModalConfirmOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md) | string | | +| [buttonColor](./kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md) | EuiConfirmModalProps['buttonColor'] | | +| [cancelButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md) | string | | +| [className](./kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md) | string | | +| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md) | string | | +| [confirmButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md) | string | | +| [defaultFocusedButton](./kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md) | EuiConfirmModalProps['defaultFocusedButton'] | | +| [maxWidth](./kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md) | boolean | number | string | Sets the max-width of the modal. Set to true to use the default (euiBreakpoints 'm'), set to false to not restrict the width, set to a number for a custom width in px, set to a string for a custom width in custom measurement. | +| [title](./kibana-plugin-core-public.overlaymodalconfirmoptions.title.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.title.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.title.md new file mode 100644 index 000000000000..cfbe41e0a7e9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [title](./kibana-plugin-core-public.overlaymodalconfirmoptions.title.md) + +## OverlayModalConfirmOptions.title property + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md new file mode 100644 index 000000000000..f0eba659dc62 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) > ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) + +## OverlayModalOpenOptions."data-test-subj" property + +Signature: + +```typescript +'data-test-subj'?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.classname.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.classname.md new file mode 100644 index 000000000000..769387b8c35f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.classname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) > [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) + +## OverlayModalOpenOptions.className property + +Signature: + +```typescript +className?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md new file mode 100644 index 000000000000..4e685055b9e1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) > [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) + +## OverlayModalOpenOptions.closeButtonAriaLabel property + +Signature: + +```typescript +closeButtonAriaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md new file mode 100644 index 000000000000..5c0ef8fb1ec8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) + +## OverlayModalOpenOptions interface + + +Signature: + +```typescript +export interface OverlayModalOpenOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) | string | | +| [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) | string | | +| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.md new file mode 100644 index 000000000000..1d8fe1a92dd9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) + +## OverlayModalStart interface + +APIs to open and manage modal dialogs. + +Signature: + +```typescript +export interface OverlayModalStart +``` + +## Methods + +| Method | Description | +| --- | --- | +| [open(mount, options)](./kibana-plugin-core-public.overlaymodalstart.open.md) | Opens a modal panel with the given mount point inside. You can use close() on the returned OverlayRef to close the modal. | +| [openConfirm(message, options)](./kibana-plugin-core-public.overlaymodalstart.openconfirm.md) | Opens a confirmation modal with the given text or mountpoint as a message. Returns a Promise resolving to true if user confirmed or false otherwise. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md new file mode 100644 index 000000000000..1c6b82e37a62 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) > [open](./kibana-plugin-core-public.overlaymodalstart.open.md) + +## OverlayModalStart.open() method + +Opens a modal panel with the given mount point inside. You can use `close()` on the returned OverlayRef to close the modal. + +Signature: + +```typescript +open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mount | MountPoint | | +| options | OverlayModalOpenOptions | | + +Returns: + +`OverlayRef` + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md new file mode 100644 index 000000000000..b0052c0f6460 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) > [openConfirm](./kibana-plugin-core-public.overlaymodalstart.openconfirm.md) + +## OverlayModalStart.openConfirm() method + +Opens a confirmation modal with the given text or mountpoint as a message. Returns a Promise resolving to `true` if user confirmed or `false` otherwise. + +Signature: + +```typescript +openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| message | MountPoint | string | | +| options | OverlayModalConfirmOptions | | + +Returns: + +`Promise` + diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 1393e69d55e5..564bbd712c53 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -167,7 +167,16 @@ export { IHttpResponseInterceptorOverrides, } from './http'; -export { OverlayStart, OverlayBannersStart, OverlayRef } from './overlays'; +export { + OverlayStart, + OverlayBannersStart, + OverlayRef, + OverlayFlyoutStart, + OverlayFlyoutOpenOptions, + OverlayModalOpenOptions, + OverlayModalConfirmOptions, + OverlayModalStart, +} from './overlays'; export { Toast, diff --git a/src/core/public/overlays/index.ts b/src/core/public/overlays/index.ts index 417486f78f71..31b524d85abb 100644 --- a/src/core/public/overlays/index.ts +++ b/src/core/public/overlays/index.ts @@ -20,5 +20,5 @@ export { OverlayRef } from './types'; export { OverlayBannersStart } from './banners'; export { OverlayFlyoutStart, OverlayFlyoutOpenOptions } from './flyout'; -export { OverlayModalStart, OverlayModalOpenOptions } from './modal'; +export { OverlayModalStart, OverlayModalOpenOptions, OverlayModalConfirmOptions } from './modal'; export { OverlayService, OverlayStart } from './overlay_service'; diff --git a/src/core/public/overlays/modal/index.ts b/src/core/public/overlays/modal/index.ts index 9ef4492af3a3..4e270838eae4 100644 --- a/src/core/public/overlays/modal/index.ts +++ b/src/core/public/overlays/modal/index.ts @@ -17,4 +17,9 @@ * under the License. */ -export { ModalService, OverlayModalStart, OverlayModalOpenOptions } from './modal_service'; +export { + ModalService, + OverlayModalStart, + OverlayModalOpenOptions, + OverlayModalConfirmOptions, +} from './modal_service'; diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index f3bbd5c94bdb..4c0c205ae543 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -70,6 +70,14 @@ export interface OverlayModalConfirmOptions { 'data-test-subj'?: string; defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; buttonColor?: EuiConfirmModalProps['buttonColor']; + /** + * Sets the max-width of the modal. + * Set to `true` to use the default (`euiBreakpoints 'm'`), + * set to `false` to not restrict the width, + * set to a number for a custom width in px, + * set to a string for a custom width in custom measurement. + */ + maxWidth?: boolean | number | string; } /** diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 28a20845426d..37e57a9ee606 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -862,6 +862,60 @@ export interface OverlayBannersStart { replace(id: string | undefined, mount: MountPoint, priority?: number): string; } +// @public (undocumented) +export interface OverlayFlyoutOpenOptions { + // (undocumented) + 'data-test-subj'?: string; + // (undocumented) + className?: string; + // (undocumented) + closeButtonAriaLabel?: string; + // (undocumented) + ownFocus?: boolean; +} + +// @public +export interface OverlayFlyoutStart { + open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; +} + +// @public (undocumented) +export interface OverlayModalConfirmOptions { + // (undocumented) + 'data-test-subj'?: string; + // (undocumented) + buttonColor?: EuiConfirmModalProps['buttonColor']; + // (undocumented) + cancelButtonText?: string; + // (undocumented) + className?: string; + // (undocumented) + closeButtonAriaLabel?: string; + // (undocumented) + confirmButtonText?: string; + // (undocumented) + defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; + maxWidth?: boolean | number | string; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export interface OverlayModalOpenOptions { + // (undocumented) + 'data-test-subj'?: string; + // (undocumented) + className?: string; + // (undocumented) + closeButtonAriaLabel?: string; +} + +// @public +export interface OverlayModalStart { + open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef; + openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; +} + // @public export interface OverlayRef { close(): Promise; @@ -874,12 +928,8 @@ export interface OverlayStart { banners: OverlayBannersStart; // (undocumented) openConfirm: OverlayModalStart['openConfirm']; - // Warning: (ae-forgotten-export) The symbol "OverlayFlyoutStart" needs to be exported by the entry point index.d.ts - // // (undocumented) openFlyout: OverlayFlyoutStart['open']; - // Warning: (ae-forgotten-export) The symbol "OverlayModalStart" needs to be exported by the entry point index.d.ts - // // (undocumented) openModal: OverlayModalStart['open']; } diff --git a/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts b/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts index 80d2dbc0b156..7f6e2a12d9e5 100644 --- a/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts +++ b/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts @@ -6,6 +6,7 @@ import { SavedObject, SavedObjectReference } from 'src/core/types'; import { Tag, TagAttributes } from '../types'; +import { TagsCapabilities } from '../capabilities'; export const createTagReference = (id: string): SavedObjectReference => ({ type: 'tag', @@ -35,3 +36,13 @@ export const createTagAttributes = (parts: Partial = {}): TagAttr color: '#FF00CC', ...parts, }); + +export const createTagCapabilities = (parts: Partial = {}): TagsCapabilities => ({ + view: true, + create: true, + edit: true, + delete: true, + assign: true, + viewConnections: true, + ...parts, +}); diff --git a/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx b/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx index 7baebdae2493..1a80c0598f97 100644 --- a/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx @@ -22,6 +22,7 @@ import { EuiTextArea, EuiSpacer, EuiText, + htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -52,6 +53,7 @@ export const CreateOrEditModal: FC = ({ tag, mode, }) => { + const optionalMessageId = htmlIdGenerator()(); const ifMounted = useIfMounted(); const [submitting, setSubmitting] = useState(false); @@ -139,6 +141,12 @@ export const CreateOrEditModal: FC = ({ onClick={() => setColor(getRandomColor())} size="xs" style={{ height: '18px', fontSize: '0.75rem' }} + aria-label={i18n.translate( + 'xpack.savedObjectsTagging.management.createModal.color.randomizeAriaLabel', + { + defaultMessage: 'Randomize tag color', + } + )} > = ({ defaultMessage: 'Description', })} labelAppend={ - + = ({ resize="none" fullWidth={true} compressed={true} + aria-describedby={optionalMessageId} /> diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.test.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.test.ts new file mode 100644 index 000000000000..42a4e628bef4 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + overlayServiceMock, + notificationServiceMock, +} from '../../../../../../src/core/public/mocks'; +import { tagClientMock } from '../../tags/tags_client.mock'; +import { TagBulkAction } from '../types'; +import { getBulkDeleteAction } from './bulk_delete'; + +describe('bulkDeleteAction', () => { + let tagClient: ReturnType; + let overlays: ReturnType; + let notifications: ReturnType; + let setLoading: jest.MockedFunction<(loading: boolean) => void>; + let action: TagBulkAction; + + const tagIds = ['id-1', 'id-2', 'id-3']; + + beforeEach(() => { + tagClient = tagClientMock.create(); + overlays = overlayServiceMock.createStartContract(); + notifications = notificationServiceMock.createStartContract(); + setLoading = jest.fn(); + + action = getBulkDeleteAction({ tagClient, overlays, notifications, setLoading }); + }); + + it('performs the operation if the confirmation is accepted', async () => { + overlays.openConfirm.mockResolvedValue(true); + + await action.execute(tagIds); + + expect(overlays.openConfirm).toHaveBeenCalledTimes(1); + + expect(tagClient.bulkDelete).toHaveBeenCalledTimes(1); + expect(tagClient.bulkDelete).toHaveBeenCalledWith(tagIds); + + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + }); + + it('does not perform the operation if the confirmation is rejected', async () => { + overlays.openConfirm.mockResolvedValue(false); + + await action.execute(tagIds); + + expect(overlays.openConfirm).toHaveBeenCalledTimes(1); + + expect(tagClient.bulkDelete).not.toHaveBeenCalled(); + expect(notifications.toasts.addSuccess).not.toHaveBeenCalled(); + }); + + it('does not show notification if `client.bulkDelete` rejects ', async () => { + overlays.openConfirm.mockResolvedValue(true); + tagClient.bulkDelete.mockRejectedValue(new Error('error calling bulkDelete')); + + await expect(action.execute(tagIds)).rejects.toMatchInlineSnapshot( + `[Error: error calling bulkDelete]` + ); + + expect(notifications.toasts.addSuccess).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.ts new file mode 100644 index 000000000000..6d9c14d33000 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OverlayStart, NotificationsStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import { ITagInternalClient } from '../../tags'; +import { TagBulkAction } from '../types'; + +interface GetBulkDeleteActionOptions { + overlays: OverlayStart; + notifications: NotificationsStart; + tagClient: ITagInternalClient; + setLoading: (loading: boolean) => void; +} + +export const getBulkDeleteAction = ({ + overlays, + notifications, + tagClient, + setLoading, +}: GetBulkDeleteActionOptions): TagBulkAction => { + return { + id: 'delete', + label: i18n.translate('xpack.savedObjectsTagging.management.actions.bulkDelete.label', { + defaultMessage: 'Delete', + }), + 'aria-label': i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.ariaLabel', + { + defaultMessage: 'Delete selected tags', + } + ), + icon: 'trash', + refreshAfterExecute: true, + execute: async (tagIds) => { + const confirmed = await overlays.openConfirm( + i18n.translate('xpack.savedObjectsTagging.management.actions.bulkDelete.confirm.text', { + defaultMessage: + 'By deleting {count, plural, one {this tag} other {these tags}}, you will no longer be able to assign {count, plural, one {it} other {them}} to saved objects. ' + + '{count, plural, one {This tag} other {These tags}} will be removed from any saved objects that currently use {count, plural, one {it} other {them}}. ' + + 'Are you sure you wish to proceed?', + values: { + count: tagIds.length, + }, + }), + { + title: i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.confirm.title', + { + defaultMessage: 'Delete {count, plural, one {1 tag} other {# tags}}', + values: { + count: tagIds.length, + }, + } + ), + confirmButtonText: i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.confirm.confirmButtonText', + { + defaultMessage: 'Delete {count, plural, one {tag} other {tags}}', + values: { + count: tagIds.length, + }, + } + ), + buttonColor: 'danger', + maxWidth: 560, + } + ); + + if (confirmed) { + setLoading(true); + await tagClient.bulkDelete(tagIds); + setLoading(false); + + notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.notification.successTitle', + { + defaultMessage: 'Deleted {count, plural, one {1 tag} other {# tags}}', + values: { + count: tagIds.length, + }, + } + ), + }); + } + }, + }; +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/clear_selection.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/clear_selection.ts new file mode 100644 index 000000000000..79212be98236 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/clear_selection.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { TagBulkAction } from '../types'; + +interface GetClearSelectionActionOptions { + clearSelection: () => void; +} + +export const getClearSelectionAction = ({ + clearSelection, +}: GetClearSelectionActionOptions): TagBulkAction => { + return { + id: 'clear_selection', + label: i18n.translate('xpack.savedObjectsTagging.management.actions.clearSelection.label', { + defaultMessage: 'Clear selection', + }), + icon: 'cross', + refreshAfterExecute: true, + execute: async () => { + clearSelection(); + }, + }; +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/index.test.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.test.ts new file mode 100644 index 000000000000..5325d4ee97cf --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { createTagCapabilities } from '../../../common/test_utils'; +import { TagsCapabilities } from '../../../common/capabilities'; +import { tagClientMock } from '../../tags/tags_client.mock'; +import { TagBulkAction } from '../types'; + +import { getBulkActions } from './index'; + +describe('getBulkActions', () => { + let core: ReturnType; + let tagClient: ReturnType; + let clearSelection: jest.MockedFunction<() => void>; + let setLoading: jest.MockedFunction<(loading: boolean) => void>; + + beforeEach(() => { + core = coreMock.createStart(); + tagClient = tagClientMock.create(); + clearSelection = jest.fn(); + setLoading = jest.fn(); + }); + + const getActions = (caps: Partial) => + getBulkActions({ + core, + tagClient, + clearSelection, + setLoading, + capabilities: createTagCapabilities(caps), + }); + + const getIds = (actions: TagBulkAction[]) => actions.map((action) => action.id); + + it('only returns the `delete` action if user got `delete` permission', () => { + let actions = getActions({ delete: true }); + + expect(getIds(actions)).toContain('delete'); + + actions = getActions({ delete: false }); + + expect(getIds(actions)).not.toContain('delete'); + }); +}); diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/index.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.ts new file mode 100644 index 000000000000..182f0013251d --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from 'src/core/public'; +import { TagsCapabilities } from '../../../common'; +import { ITagInternalClient } from '../../tags'; +import { TagBulkAction } from '../types'; +import { getBulkDeleteAction } from './bulk_delete'; +import { getClearSelectionAction } from './clear_selection'; + +interface GetBulkActionOptions { + core: CoreStart; + capabilities: TagsCapabilities; + tagClient: ITagInternalClient; + clearSelection: () => void; + setLoading: (loading: boolean) => void; +} + +export const getBulkActions = ({ + core: { notifications, overlays }, + capabilities, + tagClient, + clearSelection, + setLoading, +}: GetBulkActionOptions): TagBulkAction[] => { + const actions: TagBulkAction[] = []; + + if (capabilities.delete) { + actions.push(getBulkDeleteAction({ notifications, overlays, tagClient, setLoading })); + } + + // only add clear selection if user has permission to perform any other action + // as having at least one action will show the bulk action menu, and the selection column on the table + // and we want to avoid doing that only for the 'unselect' action. + if (actions.length > 0) { + actions.push(getClearSelectionAction({ clearSelection })); + } + + return actions; +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/_action_bar.scss b/x-pack/plugins/saved_objects_tagging/public/management/components/_action_bar.scss new file mode 100644 index 000000000000..6858e70e49e8 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/_action_bar.scss @@ -0,0 +1,17 @@ +.tagMgt__actionBar + .euiSpacer { + display: none; +} + +.tagMgt__actionBarDivider { + height: $euiSize; + border-right: $euiBorderThin; +} + +.tagMgt__actionBar { + border-bottom: $euiBorderThin; + padding-bottom: $euiSizeS; +} + +.tagMgt__actionBarIcon { + margin-left: $euiSizeXS; +} diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/action_bar.tsx b/x-pack/plugins/saved_objects_tagging/public/management/components/action_bar.tsx new file mode 100644 index 000000000000..15d8f155f624 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/action_bar.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo, FC } from 'react'; +import { + EuiPopover, + EuiFlexItem, + EuiFlexGroup, + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiText, + EuiLink, + EuiIcon, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TagBulkAction } from '../types'; + +import './_action_bar.scss'; + +export interface ActionBarProps { + actions: TagBulkAction[]; + totalCount: number; + selectedCount: number; + onActionSelected: (action: TagBulkAction) => void; +} + +const actionToMenuItem = ( + action: TagBulkAction, + onActionSelected: (action: TagBulkAction) => void, + closePopover: () => void +): EuiContextMenuPanelItemDescriptor => { + return { + name: action.label, + icon: action.icon, + onClick: () => { + closePopover(); + onActionSelected(action); + }, + 'data-test-subj': `actionBar-button-${action.id}`, + }; +}; + +export const ActionBar: FC = ({ + actions, + onActionSelected, + selectedCount, + totalCount, +}) => { + const [isPopoverOpened, setPopOverOpened] = useState(false); + + const closePopover = useCallback(() => { + setPopOverOpened(false); + }, [setPopOverOpened]); + + const togglePopover = useCallback(() => { + setPopOverOpened((opened) => !opened); + }, [setPopOverOpened]); + + const contextMenuPanels = useMemo(() => { + return [ + { + id: 0, + items: actions.map((action) => actionToMenuItem(action, onActionSelected, closePopover)), + }, + ]; + }, [actions, onActionSelected, closePopover]); + + return ( +
+ + + + + + + {selectedCount > 0 && ( + <> + +
+ + + + + + + + + } + > + + + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts b/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts index 8435aa0431c2..a28e3523d7af 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts @@ -6,3 +6,4 @@ export { Header } from './header'; export { TagTable } from './table'; +export { ActionBar } from './action_bar'; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx b/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx index e86977c60ade..ed1903fca249 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useRef, useEffect, FC } from 'react'; -import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; +import React, { useRef, useEffect, FC, ReactNode } from 'react'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink, Query } from '@elastic/eui'; import { Action as EuiTableAction } from '@elastic/eui/src/components/basic_table/action_types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -16,12 +16,16 @@ interface TagTableProps { loading: boolean; capabilities: TagsCapabilities; tags: TagWithRelations[]; + initialQuery?: Query; + allowSelection: boolean; + onQueryChange: (query?: Query) => void; selectedTags: TagWithRelations[]; onSelectionChange: (selection: TagWithRelations[]) => void; onEdit: (tag: TagWithRelations) => void; onDelete: (tag: TagWithRelations) => void; getTagRelationUrl: (tag: TagWithRelations) => string; onShowRelations: (tag: TagWithRelations) => void; + actionBar: ReactNode; } const tablePagination = { @@ -43,11 +47,16 @@ export const TagTable: FC = ({ loading, capabilities, tags, + initialQuery, + allowSelection, + onQueryChange, selectedTags, + onSelectionChange, onEdit, onDelete, onShowRelations, getTagRelationUrl, + actionBar, }) => { const tableRef = useRef>(null); @@ -60,9 +69,11 @@ export const TagTable: FC = ({ const actions: Array> = []; if (capabilities.edit) { actions.push({ - name: i18n.translate('xpack.savedObjectsTagging.management.table.actions.edit.title', { - defaultMessage: 'Edit', - }), + name: ({ name }) => + i18n.translate('xpack.savedObjectsTagging.management.table.actions.edit.title', { + defaultMessage: 'Edit {name} tag', + values: { name }, + }), description: i18n.translate( 'xpack.savedObjectsTagging.management.table.actions.edit.description', { @@ -77,9 +88,11 @@ export const TagTable: FC = ({ } if (capabilities.delete) { actions.push({ - name: i18n.translate('xpack.savedObjectsTagging.management.table.actions.delete.title', { - defaultMessage: 'Delete', - }), + name: ({ name }) => + i18n.translate('xpack.savedObjectsTagging.management.table.actions.delete.title', { + defaultMessage: 'Delete {name} tag', + values: { name }, + }), description: i18n.translate( 'xpack.savedObjectsTagging.management.table.actions.delete.description', { @@ -171,13 +184,30 @@ export const TagTable: FC = ({ { + onQueryChange(query || undefined); + }, box: { 'data-test-subj': 'tagsManagementSearchBar', incremental: true, diff --git a/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx b/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx index 4afb15bec624..6b0e17a945c0 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx @@ -6,13 +6,15 @@ import React, { useEffect, useCallback, useState, useMemo, FC } from 'react'; import useMount from 'react-use/lib/useMount'; -import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContent, Query } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb, CoreStart } from 'src/core/public'; import { TagWithRelations, TagsCapabilities } from '../../common'; import { getCreateModalOpener, getEditModalOpener } from '../components/edition_modal'; import { ITagInternalClient } from '../tags'; -import { Header, TagTable } from './components'; +import { TagBulkAction } from './types'; +import { Header, TagTable, ActionBar } from './components'; +import { getBulkActions } from './actions'; import { getTagConnectionsUrl } from './utils'; interface TagManagementPageParams { @@ -32,6 +34,21 @@ export const TagManagementPage: FC = ({ const [loading, setLoading] = useState(false); const [allTags, setAllTags] = useState([]); const [selectedTags, setSelectedTags] = useState([]); + const [query, setQuery] = useState(); + + const filteredTags = useMemo(() => { + return query ? Query.execute(query, allTags) : allTags; + }, [allTags, query]); + + const bulkActions = useMemo(() => { + return getBulkActions({ + core, + capabilities, + tagClient, + setLoading, + clearSelection: () => setSelectedTags([]), + }); + }, [core, capabilities, tagClient]); const createModalOpener = useMemo(() => getCreateModalOpener({ overlays, tagClient }), [ overlays, @@ -140,13 +157,12 @@ export const TagManagementPage: FC = ({ } ), buttonColor: 'danger', + maxWidth: 560, } ); if (confirmed) { await tagClient.delete(tag.id); - fetchTags(); - notifications.toasts.addSuccess({ title: i18n.translate('xpack.savedObjectsTagging.notifications.deleteTagSuccessTitle', { defaultMessage: 'Deleted "{name}" tag', @@ -155,18 +171,59 @@ export const TagManagementPage: FC = ({ }, }), }); + + await fetchTags(); } }, [overlays, notifications, fetchTags, tagClient] ); + const executeBulkAction = useCallback( + async (action: TagBulkAction) => { + try { + await action.execute(selectedTags.map(({ id }) => id)); + } catch (e) { + notifications.toasts.addError(e, { + title: i18n.translate('xpack.savedObjectsTagging.notifications.bulkActionError', { + defaultMessage: 'An error occurred', + }), + }); + } finally { + setLoading(false); + } + if (action.refreshAfterExecute) { + await fetchTags(); + } + }, + [selectedTags, fetchTags, notifications] + ); + + const actionBar = useMemo( + () => ( + + ), + [selectedTags, filteredTags, bulkActions, executeBulkAction] + ); + return (
{ + setQuery(newQuery); + setSelectedTags([]); + }} + allowSelection={bulkActions.length > 0} selectedTags={selectedTags} onSelectionChange={(tags) => { setSelectedTags(tags); diff --git a/x-pack/plugins/saved_objects_tagging/public/management/types.ts b/x-pack/plugins/saved_objects_tagging/public/management/types.ts new file mode 100644 index 000000000000..fc1578514243 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; + +/** + * Represents a tag `bulk action` + */ +export interface TagBulkAction { + /** + * The unique identifier for this action. + */ + id: string; + /** + * The label displayed in the bulk action context menu. + */ + label: string; + /** + * Optional aria-label if the visual label isn't descriptive enough. + */ + 'aria-label'?: string; + /** + * An optional icon to display before the label in the context menu. + */ + icon?: EuiIconType; + /** + * Handler to execute this action against the given list of selected tag ids. + */ + execute: (tagIds: string[]) => void | Promise; + /** + * If true, the list of tags will be reloaded after the action's execution. Defaults to false. + */ + refreshAfterExecute?: boolean; +} diff --git a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.mock.ts b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.mock.ts new file mode 100644 index 000000000000..4ef0e89ae486 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ITagInternalClient } from './tags_client'; + +const createInternalClientMock = () => { + const mock: jest.Mocked = { + create: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + find: jest.fn(), + bulkDelete: jest.fn(), + }; + + return mock; +}; + +export const tagClientMock = { + create: createInternalClientMock, +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts index ac73880e5294..576f89b79601 100644 --- a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts +++ b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts @@ -216,41 +216,83 @@ describe('TagsClient', () => { }); }); - ///// + describe('internal APIs', () => { + describe('#find', () => { + const findOptions: FindTagsOptions = { + search: 'for, you know.', + }; + let expectedTags: Tag[]; - describe('#find', () => { - const findOptions: FindTagsOptions = { - search: 'for, you know.', - }; - let expectedTags: Tag[]; + beforeEach(() => { + expectedTags = [ + createTag({ id: 'tag-1' }), + createTag({ id: 'tag-2' }), + createTag({ id: 'tag-3' }), + ]; + http.get.mockResolvedValue({ tags: expectedTags, total: expectedTags.length }); + }); - beforeEach(() => { - expectedTags = [ - createTag({ id: 'tag-1' }), - createTag({ id: 'tag-2' }), - createTag({ id: 'tag-3' }), - ]; - http.get.mockResolvedValue({ tags: expectedTags, total: expectedTags.length }); - }); + it('calls `http.get` with the correct parameters', async () => { + await tagsClient.find(findOptions); - it('calls `http.get` with the correct parameters', async () => { - await tagsClient.find(findOptions); + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith(`/internal/saved_objects_tagging/tags/_find`, { + query: findOptions, + }); + }); + it('returns the tag objects from the response', async () => { + const { tags, total } = await tagsClient.find(findOptions); + expect(tags).toEqual(expectedTags); + expect(total).toEqual(3); + }); + it('forwards the error from the http call if any', async () => { + const error = new Error('something when wrong'); + http.get.mockRejectedValue(error); - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`/internal/saved_objects_tagging/tags/_find`, { - query: findOptions, + await expect(tagsClient.find(findOptions)).rejects.toThrowError(error); }); }); - it('returns the tag objects from the response', async () => { - const { tags, total } = await tagsClient.find(findOptions); - expect(tags).toEqual(expectedTags); - expect(total).toEqual(3); - }); - it('forwards the error from the http call if any', async () => { - const error = new Error('something when wrong'); - http.get.mockRejectedValue(error); - await expect(tagsClient.find(findOptions)).rejects.toThrowError(error); + describe('#bulkDelete', () => { + const tagIds = ['id-to-delete-1', 'id-to-delete-2']; + + beforeEach(() => { + http.post.mockResolvedValue({}); + }); + + it('calls `http.post` with the correct parameters', async () => { + await tagsClient.bulkDelete(tagIds); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith( + `/internal/saved_objects_tagging/tags/_bulk_delete`, + { + body: JSON.stringify({ + ids: tagIds, + }), + } + ); + }); + it('forwards the error from the http call if any', async () => { + const error = new Error('something when wrong'); + http.post.mockRejectedValue(error); + + await expect(tagsClient.bulkDelete(tagIds)).rejects.toThrowError(error); + }); + it('notifies its changeListener if the http call succeed', async () => { + await tagsClient.bulkDelete(tagIds); + + expect(changeListener.onDelete).toHaveBeenCalledTimes(2); + expect(changeListener.onDelete).toHaveBeenCalledWith(tagIds[0]); + expect(changeListener.onDelete).toHaveBeenCalledWith(tagIds[1]); + }); + it('ignores potential errors when calling `changeListener.onDelete`', async () => { + changeListener.onDelete.mockImplementation(() => { + throw new Error('error in onCreate'); + }); + + await expect(tagsClient.bulkDelete(tagIds)).resolves.toBeUndefined(); + }); }); }); }); diff --git a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts index 3169babb2bae..a866ae82f970 100644 --- a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts +++ b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts @@ -34,6 +34,7 @@ const trapErrors = (fn: () => void) => { export interface ITagInternalClient extends ITagsClient { find(options: FindTagsOptions): Promise; + bulkDelete(ids: string[]): Promise; } export class TagsClient implements ITagInternalClient { @@ -114,4 +115,20 @@ export class TagsClient implements ITagInternalClient { }, }); } + + public async bulkDelete(tagIds: string[]) { + await this.http.post<{}>('/internal/saved_objects_tagging/tags/_bulk_delete', { + body: JSON.stringify({ + ids: tagIds, + }), + }); + + trapErrors(() => { + if (this.changeListener) { + tagIds.forEach((tagId) => { + this.changeListener!.onDelete(tagId); + }); + } + }); + } } diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/index.ts b/x-pack/plugins/saved_objects_tagging/server/routes/index.ts index 9519f54e0169..facfb3f690a2 100644 --- a/x-pack/plugins/saved_objects_tagging/server/routes/index.ts +++ b/x-pack/plugins/saved_objects_tagging/server/routes/index.ts @@ -10,7 +10,7 @@ import { registerDeleteTagRoute } from './delete_tag'; import { registerGetAllTagsRoute } from './get_all_tags'; import { registerGetTagRoute } from './get_tag'; import { registerUpdateTagRoute } from './update_tag'; -import { registerInternalFindTagsRoute } from './internal'; +import { registerInternalFindTagsRoute, registerInternalBulkDeleteRoute } from './internal'; export const registerRoutes = ({ router }: { router: IRouter }) => { // public API @@ -21,4 +21,5 @@ export const registerRoutes = ({ router }: { router: IRouter }) => { registerGetTagRoute(router); // internal API registerInternalFindTagsRoute(router); + registerInternalBulkDeleteRoute(router); }; diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/internal/bulk_delete.ts b/x-pack/plugins/saved_objects_tagging/server/routes/internal/bulk_delete.ts new file mode 100644 index 000000000000..bade81678543 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/routes/internal/bulk_delete.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export const registerInternalBulkDeleteRoute = (router: IRouter) => { + router.post( + { + path: '/internal/saved_objects_tagging/tags/_bulk_delete', + validate: { + body: schema.object({ + ids: schema.arrayOf(schema.string()), + }), + }, + }, + router.handleLegacyErrors(async (ctx, req, res) => { + const { ids: tagIds } = req.body; + const client = ctx.tags!.tagsClient; + + for (const tagId of tagIds) { + await client.delete(tagId); + } + + return res.ok({ + body: {}, + }); + }) + ); +}; diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts b/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts index 9d427cfe5831..e20403af1f59 100644 --- a/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts +++ b/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts @@ -5,3 +5,4 @@ */ export { registerInternalFindTagsRoute } from './find_tags'; +export { registerInternalBulkDeleteRoute } from './bulk_delete'; diff --git a/x-pack/test/functional/page_objects/tag_management_page.ts b/x-pack/test/functional/page_objects/tag_management_page.ts index 8b354e9d0e1c..7d40bf5600da 100644 --- a/x-pack/test/functional/page_objects/tag_management_page.ts +++ b/x-pack/test/functional/page_objects/tag_management_page.ts @@ -156,6 +156,13 @@ export function TagManagementPageProvider({ getService, getPageObjects }: FtrPro } } + /** + * Tag management page object. + * + * @remarks All the table manipulation helpers makes the assumption + * that all tags are displayed on a single page. Pagination + * and finding / interacting with a tag on another page is not supported. + */ class TagManagementPage { public readonly tagModal = new TagModal(this); @@ -272,6 +279,90 @@ export function TagManagementPageProvider({ getService, getPageObjects }: FtrPro await connectionLink.click(); } + /** + * Return true if the selection column is displayed on the table, false otherwise. + */ + async isSelectionColumnDisplayed() { + const firstRow = await testSubjects.find('tagsTableRow'); + const checkbox = await firstRow.findAllByCssSelector( + '.euiTableRowCellCheckbox .euiCheckbox__input' + ); + return Boolean(checkbox.length); + } + + /** + * Click on the selection checkbox of the tag matching given tag name. + */ + async selectTagByName(tagName: string) { + const tagRow = await this.getRowByName(tagName); + const checkbox = await tagRow.findByCssSelector( + '.euiTableRowCellCheckbox .euiCheckbox__input' + ); + await checkbox.click(); + } + + /** + * Returns true if the tag bulk action menu is displayed, false otherwise. + */ + async isActionMenuButtonDisplayed() { + return testSubjects.exists('actionBar-contextMenuButton'); + } + + /** + * Open the bulk action menu if not already opened. + */ + async openActionMenu() { + if (!(await this.isActionMenuOpened())) { + await this.toggleActionMenu(); + } + } + + /** + * Check if the action for given `actionId` is present in the bulk action menu. + * + * The menu will automatically be opened if not already, but the test must still + * select tags to make the action menu button appear. + */ + async isActionPresent(actionId: string) { + if (!(await this.isActionMenuButtonDisplayed())) { + return false; + } + const menuWasOpened = await this.isActionMenuOpened(); + if (!menuWasOpened) { + await this.openActionMenu(); + } + + const actionExists = await testSubjects.exists(`actionBar-button-${actionId}`); + + if (!menuWasOpened) { + await this.toggleActionMenu(); + } + + return actionExists; + } + + /** + * Click on given bulk action button + */ + async clickOnAction(actionId: string) { + await this.openActionMenu(); + await testSubjects.click(`actionBar-button-${actionId}`); + } + + /** + * Toggle (close if opened, open if closed) the bulk action menu. + */ + async toggleActionMenu() { + await testSubjects.click('actionBar-contextMenuButton'); + } + + /** + * Return true if the bulk action menu is opened, false otherwise. + */ + async isActionMenuOpened() { + return testSubjects.exists('actionBar-contextMenuPopover'); + } + /** * Return the info of all the tags currently displayed in the table (in table's order) */ diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_bulk_delete.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_bulk_delete.ts new file mode 100644 index 000000000000..0c9a06bd5882 --- /dev/null +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_bulk_delete.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { USERS, User, ExpectedResponse } from '../../../common/lib'; +import { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + + describe('POST /internal/saved_objects_tagging/tags/_bulk_delete', () => { + beforeEach(async () => { + await esArchiver.load('rbac_tags'); + }); + + afterEach(async () => { + await esArchiver.unload('rbac_tags'); + }); + + const responses: Record = { + authorized: { + httpCode: 200, + expectResponse: ({ body }) => { + expect(body).to.eql({}); + }, + }, + unauthorized: { + httpCode: 403, + expectResponse: ({ body }) => { + expect(body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unable to delete tag', + }); + }, + }, + }; + + const expectedResults: Record = { + authorized: [ + USERS.SUPERUSER, + USERS.DEFAULT_SPACE_SO_MANAGEMENT_WRITE_USER, + USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, + ], + unauthorized: [ + USERS.DEFAULT_SPACE_READ_USER, + USERS.DEFAULT_SPACE_SO_TAGGING_READ_USER, + USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, + USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.NOT_A_KIBANA_USER, + ], + }; + + const createUserTest = ( + { username, password, description }: User, + { httpCode, expectResponse }: ExpectedResponse + ) => { + it(`returns expected ${httpCode} response for ${description ?? username}`, async () => { + await supertest + .post(`/internal/saved_objects_tagging/tags/_bulk_delete`) + .send({ + ids: ['default-space-tag-1', 'default-space-tag-2'], + }) + .auth(username, password) + .expect(httpCode) + .then(expectResponse); + }); + }; + + const createTestSuite = () => { + Object.entries(expectedResults).forEach(([responseId, users]) => { + const response: ExpectedResponse = responses[responseId]; + users.forEach((user) => { + createUserTest(user, response); + }); + }); + }; + + createTestSuite(); + }); +} diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts index 5f3d1cf854f8..727479546431 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts @@ -22,5 +22,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./_find')); + loadTestFile(require.resolve('./_bulk_delete')); }); } diff --git a/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts b/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts new file mode 100644 index 000000000000..556130bed793 --- /dev/null +++ b/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'security', 'savedObjects', 'tagManagement']); + const tagManagementPage = PageObjects.tagManagement; + + describe('table bulk actions', () => { + beforeEach(async () => { + await esArchiver.load('functional_base'); + await tagManagementPage.navigateTo(); + }); + afterEach(async () => { + await esArchiver.unload('functional_base'); + }); + + describe('bulk delete', () => { + it('deletes multiple tags', async () => { + await tagManagementPage.selectTagByName('tag-1'); + await tagManagementPage.selectTagByName('tag-3'); + + await tagManagementPage.clickOnAction('delete'); + + await PageObjects.common.clickConfirmOnModal(); + await tagManagementPage.waitUntilTableIsLoaded(); + + const displayedTags = await tagManagementPage.getDisplayedTagNames(); + expect(displayedTags.length).to.be(3); + expect(displayedTags).to.eql(['my-favorite-tag', 'tag with whitespace', 'tag-2']); + }); + }); + + describe('clear selection', () => { + it('clears the current selection', async () => { + await tagManagementPage.selectTagByName('tag-1'); + await tagManagementPage.selectTagByName('tag-3'); + + await tagManagementPage.clickOnAction('clear_selection'); + + await tagManagementPage.waitUntilTableIsLoaded(); + + expect(await tagManagementPage.isActionMenuButtonDisplayed()).to.be(false); + }); + }); + }); +} diff --git a/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts b/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts index 72beabca59f5..65443fb517ed 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts @@ -35,6 +35,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }; + const selectSomeTags = async () => { + if (await tagManagementPage.isSelectionColumnDisplayed()) { + await tagManagementPage.selectTagByName('tag-1'); + await tagManagementPage.selectTagByName('tag-3'); + } + }; + const addFeatureControlSuite = ({ user, description, privileges }: FeatureControlUserSuite) => { const testPrefix = (allowed: boolean) => (allowed ? `can` : `can't`); @@ -57,6 +64,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await tagManagementPage.isDeleteButtonVisible()).to.be(privileges.delete); }); + it(`${testPrefix(privileges.delete)} bulk delete tags`, async () => { + await selectSomeTags(); + expect(await tagManagementPage.isActionPresent('delete')).to.be(privileges.delete); + }); + it(`${testPrefix(privileges.create)} create tag`, async () => { expect(await tagManagementPage.isCreateButtonVisible()).to.be(privileges.create); }); diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 0ddfa64d682a..7fd0605c34d7 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -17,6 +17,7 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { }); loadTestFile(require.resolve('./listing')); + loadTestFile(require.resolve('./bulk_actions')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./edit')); loadTestFile(require.resolve('./som_integration')); diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index d8bd39ac7dc5..b93859154319 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(itemNames).to.contain('My new markdown viz'); }); - it('allows to assign tags to the new visualization', async () => { + it('allows to create a tag from the tag selector', async () => { const { tagModal } = PageObjects.tagManagement; await PageObjects.visualize.navigateToNewVisualization();