diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md
index b8b1bdcdee3be..6a90fd49f1d66 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 0000000000000..d583aae0e0b19
--- /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 0000000000000..26f6db77cccea
--- /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 0000000000000..44014b7f0d816
--- /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 0000000000000..5945bca01f55f
--- /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 0000000000000..337ce2c48e1d9
--- /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 0000000000000..790fd57320413
--- /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 0000000000000..1f740410ca282
--- /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 0000000000000..3569b2153c3da
--- /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 0000000000000..5c827e19e42e1
--- /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 0000000000000..0c0b9fd48dbd6
--- /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 0000000000000..0a622aeaac418
--- /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 0000000000000..8a321a0b07b4c
--- /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 0000000000000..f84d834369f5b
--- /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 0000000000000..c5edf48b54ea8
--- /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 0000000000000..488b4eb3794fb
--- /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 0000000000000..83405a151a372
--- /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 0000000000000..cfbe41e0a7e9f
--- /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 0000000000000..f0eba659dc62b
--- /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 0000000000000..769387b8c35ff
--- /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 0000000000000..4e685055b9e17
--- /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 0000000000000..5c0ef8fb1ec86
--- /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 0000000000000..1d8fe1a92dd90
--- /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 0000000000000..1c6b82e37a624
--- /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 0000000000000..b0052c0f6460e
--- /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 1393e69d55e51..564bbd712c535 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 417486f78f719..31b524d85abbe 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 9ef4492af3a3a..4e270838eae44 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 f3bbd5c94bdb4..4c0c205ae5438 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 28a20845426d9..37e57a9ee606e 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 80d2dbc0b1566..7f6e2a12d9e53 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 7baebdae2493e..1a80c0598f97a 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 0000000000000..42a4e628bef4e
--- /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 0000000000000..6d9c14d330007
--- /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 0000000000000..79212be98236c
--- /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 0000000000000..5325d4ee97cf8
--- /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 0000000000000..182f0013251df
--- /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 0000000000000..6858e70e49e8f
--- /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 0000000000000..15d8f155f6246
--- /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 8435aa0431c23..a28e3523d7af6 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 e86977c60ade1..ed1903fca2495 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 4afb15bec6243..6b0e17a945c06 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 0000000000000..fc15785142431
--- /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 0000000000000..4ef0e89ae4866
--- /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 ac73880e52949..576f89b796010 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[];
+
+ beforeEach(() => {
+ expectedTags = [
+ createTag({ id: 'tag-1' }),
+ createTag({ id: 'tag-2' }),
+ createTag({ id: 'tag-3' }),
+ ];
+ http.get.mockResolvedValue({ tags: expectedTags, total: expectedTags.length });
+ });
- describe('#find', () => {
- const findOptions: FindTagsOptions = {
- search: 'for, you know.',
- };
- let expectedTags: Tag[];
+ it('calls `http.get` with the correct parameters', async () => {
+ await tagsClient.find(findOptions);
- beforeEach(() => {
- expectedTags = [
- createTag({ id: 'tag-1' }),
- createTag({ id: 'tag-2' }),
- createTag({ id: 'tag-3' }),
- ];
- http.get.mockResolvedValue({ tags: expectedTags, total: expectedTags.length });
+ 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);
+
+ await expect(tagsClient.find(findOptions)).rejects.toThrowError(error);
+ });
});
- it('calls `http.get` with the correct parameters', async () => {
- await tagsClient.find(findOptions);
+ describe('#bulkDelete', () => {
+ const tagIds = ['id-to-delete-1', 'id-to-delete-2'];
- expect(http.get).toHaveBeenCalledTimes(1);
- expect(http.get).toHaveBeenCalledWith(`/internal/saved_objects_tagging/tags/_find`, {
- query: findOptions,
+ beforeEach(() => {
+ http.post.mockResolvedValue({});
});
- });
- 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);
+ 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 3169babb2bae8..a866ae82f9702 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 9519f54e01693..facfb3f690a28 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 0000000000000..bade81678543d
--- /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 9d427cfe5831c..e20403af1f59b 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 8b354e9d0e1c4..7d40bf5600da4 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 0000000000000..0c9a06bd58828
--- /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 5f3d1cf854f82..727479546431c 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 0000000000000..556130bed7931
--- /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 72beabca59f5c..65443fb517edf 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 0ddfa64d682a8..7fd0605c34d76 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 d8bd39ac7dc53..b938591543196 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();