Add tag bulk action context menu (#82816)
* add the delete tag bulk action * add unit tests for bulk delete * fix duplicate i18n key * add RBAC test on bulk delete * add functional tests * self review * design nits * add maxWidth option for confirm modal and add missing doc * change bulk delete confirm modal max width * add more missing doc * only show loading state when performing the bulk delete * use spacer instead of custom margin on horizontal rule * use link instead of button to remove custom styles * remove spacers, just use styles * add divider when action menu is displayed * set max-width for single delete confirm * a11y fixes * address nits * add aria-label to delete action Co-authored-by: Michail Yasonik <michail.yasonik@elastic.co>
This commit is contained in:
parent
27125bce30
commit
a7e5f07412
|
@ -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 <code>PluginInitializer</code>. |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
'data-test-subj'?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
className?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
closeButtonAriaLabel?: string;
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md)
|
||||
|
||||
## OverlayFlyoutOpenOptions interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface OverlayFlyoutOpenOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | <code>string</code> | |
|
||||
| [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | <code>string</code> | |
|
||||
| [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | <code>string</code> | |
|
||||
| [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | <code>boolean</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
ownFocus?: boolean;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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 <code>close()</code> on the returned FlyoutRef to close the flyout. |
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| mount | <code>MountPoint</code> | |
|
||||
| options | <code>OverlayFlyoutOpenOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`OverlayRef`
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
'data-test-subj'?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
buttonColor?: EuiConfirmModalProps['buttonColor'];
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
cancelButtonText?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
className?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
closeButtonAriaLabel?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
confirmButtonText?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton'];
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
maxWidth?: boolean | number | string;
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md)
|
||||
|
||||
## OverlayModalConfirmOptions interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface OverlayModalConfirmOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md) | <code>string</code> | |
|
||||
| [buttonColor](./kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md) | <code>EuiConfirmModalProps['buttonColor']</code> | |
|
||||
| [cancelButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md) | <code>string</code> | |
|
||||
| [className](./kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md) | <code>string</code> | |
|
||||
| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md) | <code>string</code> | |
|
||||
| [confirmButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md) | <code>string</code> | |
|
||||
| [defaultFocusedButton](./kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md) | <code>EuiConfirmModalProps['defaultFocusedButton']</code> | |
|
||||
| [maxWidth](./kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md) | <code>boolean | number | string</code> | Sets the max-width of the modal. Set to <code>true</code> to use the default (<code>euiBreakpoints 'm'</code>), set to <code>false</code> 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) | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
title?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
'data-test-subj'?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
className?: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
closeButtonAriaLabel?: string;
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md)
|
||||
|
||||
## OverlayModalOpenOptions interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface OverlayModalOpenOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) | <code>string</code> | |
|
||||
| [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) | <code>string</code> | |
|
||||
| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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 <code>close()</code> 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 <code>true</code> if user confirmed or <code>false</code> otherwise. |
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| mount | <code>MountPoint</code> | |
|
||||
| options | <code>OverlayModalOpenOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`OverlayRef`
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise<boolean>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| message | <code>MountPoint | string</code> | |
|
||||
| options | <code>OverlayModalConfirmOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<boolean>`
|
||||
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -17,4 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { ModalService, OverlayModalStart, OverlayModalOpenOptions } from './modal_service';
|
||||
export {
|
||||
ModalService,
|
||||
OverlayModalStart,
|
||||
OverlayModalOpenOptions,
|
||||
OverlayModalConfirmOptions,
|
||||
} from './modal_service';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<boolean>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface OverlayRef {
|
||||
close(): Promise<void>;
|
||||
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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<TagAttributes> = {}): TagAttr
|
|||
color: '#FF00CC',
|
||||
...parts,
|
||||
});
|
||||
|
||||
export const createTagCapabilities = (parts: Partial<TagsCapabilities> = {}): TagsCapabilities => ({
|
||||
view: true,
|
||||
create: true,
|
||||
edit: true,
|
||||
delete: true,
|
||||
assign: true,
|
||||
viewConnections: true,
|
||||
...parts,
|
||||
});
|
||||
|
|
|
@ -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<CreateOrEditModalProps> = ({
|
|||
tag,
|
||||
mode,
|
||||
}) => {
|
||||
const optionalMessageId = htmlIdGenerator()();
|
||||
const ifMounted = useIfMounted();
|
||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||
|
||||
|
@ -139,6 +141,12 @@ export const CreateOrEditModal: FC<CreateOrEditModalProps> = ({
|
|||
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',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.savedObjectsTagging.management.createModal.color.randomize"
|
||||
|
@ -165,7 +173,7 @@ export const CreateOrEditModal: FC<CreateOrEditModalProps> = ({
|
|||
defaultMessage: 'Description',
|
||||
})}
|
||||
labelAppend={
|
||||
<EuiText size="xs" color="subdued">
|
||||
<EuiText size="xs" color="subdued" id={optionalMessageId}>
|
||||
<FormattedMessage
|
||||
id="xpack.savedObjectsTagging.management.optionalFieldText"
|
||||
defaultMessage="Optional"
|
||||
|
@ -184,6 +192,7 @@ export const CreateOrEditModal: FC<CreateOrEditModalProps> = ({
|
|||
resize="none"
|
||||
fullWidth={true}
|
||||
compressed={true}
|
||||
aria-describedby={optionalMessageId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
|
|
|
@ -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<typeof tagClientMock.create>;
|
||||
let overlays: ReturnType<typeof overlayServiceMock.createStartContract>;
|
||||
let notifications: ReturnType<typeof notificationServiceMock.createStartContract>;
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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<typeof coreMock.createStart>;
|
||||
let tagClient: ReturnType<typeof tagClientMock.create>;
|
||||
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<TagsCapabilities>) =>
|
||||
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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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<ActionBarProps> = ({
|
||||
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 (
|
||||
<div className="tagMgt__actionBar">
|
||||
<EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.savedObjectsTagging.management.actionBar.totalTagsLabel"
|
||||
defaultMessage="{count, plural, one {1 tag} other {# tags}}"
|
||||
values={{
|
||||
count: totalCount,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{selectedCount > 0 && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="tagMgt__actionBarDivider" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
isOpen={isPopoverOpened}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiText size="xs">
|
||||
<EuiLink onClick={togglePopover} data-test-subj="actionBar-contextMenuButton">
|
||||
<FormattedMessage
|
||||
id="xpack.savedObjectsTagging.management.actionBar.selectedTagsLabel"
|
||||
defaultMessage="{count, plural, one {1 selected tag} other {# selected tags}}"
|
||||
values={{
|
||||
count: selectedCount,
|
||||
}}
|
||||
/>
|
||||
<EuiIcon className="tagMgt__actionBarIcon" type="arrowDown" size="s" />
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={contextMenuPanels}
|
||||
data-test-subj="actionBar-contextMenu"
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -6,3 +6,4 @@
|
|||
|
||||
export { Header } from './header';
|
||||
export { TagTable } from './table';
|
||||
export { ActionBar } from './action_bar';
|
||||
|
|
|
@ -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<TagTableProps> = ({
|
|||
loading,
|
||||
capabilities,
|
||||
tags,
|
||||
initialQuery,
|
||||
allowSelection,
|
||||
onQueryChange,
|
||||
selectedTags,
|
||||
onSelectionChange,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onShowRelations,
|
||||
getTagRelationUrl,
|
||||
actionBar,
|
||||
}) => {
|
||||
const tableRef = useRef<EuiInMemoryTable<TagWithRelations>>(null);
|
||||
|
||||
|
@ -60,9 +69,11 @@ export const TagTable: FC<TagTableProps> = ({
|
|||
const actions: Array<EuiTableAction<TagWithRelations>> = [];
|
||||
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<TagTableProps> = ({
|
|||
}
|
||||
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<TagTableProps> = ({
|
|||
<EuiInMemoryTable
|
||||
data-test-subj="tagsManagementTable"
|
||||
ref={tableRef}
|
||||
childrenBetween={actionBar}
|
||||
loading={loading}
|
||||
itemId={'id'}
|
||||
columns={columns}
|
||||
items={tags}
|
||||
pagination={tablePagination}
|
||||
sorting={sorting}
|
||||
tableCaption={i18n.translate('xpack.savedObjectsTagging.management.table.columns.caption', {
|
||||
defaultMessage: 'Tags',
|
||||
})}
|
||||
rowHeader="name"
|
||||
selection={
|
||||
allowSelection
|
||||
? {
|
||||
initialSelected: selectedTags,
|
||||
onSelectionChange,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
search={{
|
||||
defaultQuery: initialQuery,
|
||||
onChange: ({ query }) => {
|
||||
onQueryChange(query || undefined);
|
||||
},
|
||||
box: {
|
||||
'data-test-subj': 'tagsManagementSearchBar',
|
||||
incremental: true,
|
||||
|
|
|
@ -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<TagManagementPageParams> = ({
|
|||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [allTags, setAllTags] = useState<TagWithRelations[]>([]);
|
||||
const [selectedTags, setSelectedTags] = useState<TagWithRelations[]>([]);
|
||||
const [query, setQuery] = useState<Query | undefined>();
|
||||
|
||||
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<TagManagementPageParams> = ({
|
|||
}
|
||||
),
|
||||
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<TagManagementPageParams> = ({
|
|||
},
|
||||
}),
|
||||
});
|
||||
|
||||
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(
|
||||
() => (
|
||||
<ActionBar
|
||||
actions={bulkActions}
|
||||
totalCount={filteredTags.length}
|
||||
selectedCount={selectedTags.length}
|
||||
onActionSelected={executeBulkAction}
|
||||
/>
|
||||
),
|
||||
[selectedTags, filteredTags, bulkActions, executeBulkAction]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPageContent horizontalPosition="center">
|
||||
<Header canCreate={capabilities.create} onCreate={openCreateModal} />
|
||||
<TagTable
|
||||
loading={loading}
|
||||
tags={allTags}
|
||||
tags={filteredTags}
|
||||
capabilities={capabilities}
|
||||
actionBar={actionBar}
|
||||
initialQuery={query}
|
||||
onQueryChange={(newQuery) => {
|
||||
setQuery(newQuery);
|
||||
setSelectedTags([]);
|
||||
}}
|
||||
allowSelection={bulkActions.length > 0}
|
||||
selectedTags={selectedTags}
|
||||
onSelectionChange={(tags) => {
|
||||
setSelectedTags(tags);
|
||||
|
|
|
@ -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<void>;
|
||||
/**
|
||||
* If true, the list of tags will be reloaded after the action's execution. Defaults to false.
|
||||
*/
|
||||
refreshAfterExecute?: boolean;
|
||||
}
|
|
@ -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<ITagInternalClient> = {
|
||||
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,
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,6 +34,7 @@ const trapErrors = (fn: () => void) => {
|
|||
|
||||
export interface ITagInternalClient extends ITagsClient {
|
||||
find(options: FindTagsOptions): Promise<FindTagsResponse>;
|
||||
bulkDelete(ids: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export { registerInternalFindTagsRoute } from './find_tags';
|
||||
export { registerInternalBulkDeleteRoute } from './bulk_delete';
|
||||
|
|
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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<string, ExpectedResponse> = {
|
||||
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<string, User[]> = {
|
||||
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();
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue