Row trigger 2 (#83167) (#85766)

* feat: 🎸 add ROW_CLICK_TRIGGER

* feat: 🎸 wire row click event to UI Actions trigger in Lens

* feat: 🎸 add row click trigger to url drilldown

* feat: 🎸 add datatable to row click context

* feat: 🎸 pass in row index in row click trigger context

* feat: 🎸 add columns to row click trigger context

* feat: 🎸 fill values and keys event scope array

* feat: 🎸 generate correct row scope variables

* fix: 🐛 report triggers from lens embeddable

* feat: 🎸 add sample preview for row click trigger

* feat: 🎸 remove url drilldown preview box

* chore: 🤖 remove mock variable generation functions

* feat: 🎸 generate context and global variable lists

* feat: 🎸 preview event variable list

* feat: 🎸 show empty url error on blur

* feat: 🎸 add ability to always show popup for executed actions

* refactor: 💡 rename multiple action execution method

* fix: 🐛 don't add separator befor group on no main items

* feat: 🎸 wire in uiActions service into datatable renderer

* feat: 🎸 check each row if it has compatible row click actions

* feat: 🎸 allow passing data to expression renderer

* feat: 🎸 add isEmbeddable helper

* feat: 🎸 pass embeddable to lens table renderer

* feat: 🎸 hide lens table row actions which are empty

* feat: 🎸 re-render lens embeddable when dynamic actions chagne

* feat: 🎸 hide actions column if there are no row actions

* feat: 🎸 re-render lens embeddable on view mode chagne

* fix: 🐛 fix TypeScript errors

* chore: 🤖 fix TypeScript errors

* docs: ✏️ update auto-generated docs

* feat: 🎸 add hasCompatibleActions to expression layer

* feat: 🎸 remove "data" from expression renderer handlers

* fix: 🐛 fix TypeScript errors

* test: 💍 fix Jest tests

* docs: ✏️ update autogenerated docs

* fix: 🐛 wrap event payload into data

* test: 💍 add "alwaysShowPopup" test

* chore: 🤖 add comment requested in review

https://github.com/elastic/kibana/pull/83167#discussion_r537340216

* test: 💍 add hasCompatibleActions test

* test: 💍 add datatable renderer test

* test: 💍 add Lens embeddable input change tests

* test: 💍 add embeddable row click test

* fix: 🐛 add url validation

* test: 💍 add url drilldown tests

* docs: ✏️ remove url drilldown preview from docs

* docs: ✏️ remove preview from url templating

* docs: ✏️ add row click description

* chore: 🤖 move 36.5 KB bundle balance to url_drilldown

* test: 💍 simplify test case

* style: 💄 change types places

* refactor: 💡 clean up panel variable generation

* test: 💍 add getPanelVariables() tests

* fix: 🐛 generate runtime variables correctly

* fix: 🐛 improve getVariableList() and add tests for it

* feat: 🎸 add translation, improve types
# Conflicts:
#	packages/kbn-optimizer/limits.yml
This commit is contained in:
Vadim Dalecky 2020-12-14 17:22:26 +01:00 committed by GitHub
parent c47285eaea
commit 3cc2de0ac7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 1687 additions and 587 deletions

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
export declare type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T>;
export declare type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T> | RowClickContext;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [isEmbeddable](./kibana-plugin-plugins-embeddable-public.isembeddable.md)
## isEmbeddable variable
<b>Signature:</b>
```typescript
isEmbeddable: (x: unknown) => x is IEmbeddable<import("./i_embeddable").EmbeddableInput, import("./i_embeddable").EmbeddableOutput>
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [isRowClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md)
## isRowClickTriggerContext variable
<b>Signature:</b>
```typescript
isRowClickTriggerContext: (context: ChartActionContext) => context is RowClickContext
```

View file

@ -78,7 +78,9 @@
| [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | |
| [EmbeddableRenderer](./kibana-plugin-plugins-embeddable-public.embeddablerenderer.md) | Helper react component to render an embeddable Can be used if you have an embeddable object or an embeddable factory Supports updating input by passing <code>input</code> prop |
| [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) | |
| [isEmbeddable](./kibana-plugin-plugins-embeddable-public.isembeddable.md) | |
| [isRangeSelectTriggerContext](./kibana-plugin-plugins-embeddable-public.israngeselecttriggercontext.md) | |
| [isRowClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md) | |
| [isValueClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isvalueclicktriggercontext.md) | |
| [PANEL\_BADGE\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_badge_trigger.md) | |
| [PANEL\_NOTIFICATION\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_notification_trigger.md) | |

View file

@ -9,7 +9,7 @@ Constructs a new instance of the `ExpressionRenderHandler` class
<b>Signature:</b>
```typescript
constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial<ExpressionRenderHandlerParams>);
constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
```
## Parameters
@ -17,5 +17,5 @@ constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial<Expres
| Parameter | Type | Description |
| --- | --- | --- |
| element | <code>HTMLElement</code> | |
| { onRenderError, renderMode } | <code>Partial&lt;ExpressionRenderHandlerParams&gt;</code> | |
| { onRenderError, renderMode, hasCompatibleActions, } | <code>ExpressionRenderHandlerParams</code> | |

View file

@ -14,7 +14,7 @@ export declare class ExpressionRenderHandler
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(element, { onRenderError, renderMode })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the <code>ExpressionRenderHandler</code> class |
| [(constructor)(element, { onRenderError, renderMode, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the <code>ExpressionRenderHandler</code> class |
## Properties
@ -24,7 +24,7 @@ export declare class ExpressionRenderHandler
| [events$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.events_.md) | | <code>Observable&lt;ExpressionRendererEvent&gt;</code> | |
| [getElement](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.getelement.md) | | <code>() =&gt; HTMLElement</code> | |
| [handleRenderError](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.handlerendererror.md) | | <code>(error: ExpressionRenderError) =&gt; void</code> | |
| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | <code>(data: any, uiState?: any) =&gt; Promise&lt;void&gt;</code> | |
| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | <code>(value: any, uiState?: any) =&gt; Promise&lt;void&gt;</code> | |
| [render$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render_.md) | | <code>Observable&lt;number&gt;</code> | |
| [update$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.update_.md) | | <code>Observable&lt;UpdateValue &#124; null&gt;</code> | |

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
render: (data: any, uiState?: any) => Promise<void>;
render: (value: any, uiState?: any) => Promise<void>;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) &gt; [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md)
## IExpressionLoaderParams.hasCompatibleActions property
<b>Signature:</b>
```typescript
hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
```

View file

@ -19,6 +19,7 @@ export interface IExpressionLoaderParams
| [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | <code>[]</code> | |
| [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | <code>boolean</code> | |
| [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | <code>boolean</code> | |
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | <code>ExpressionRenderHandlerParams['hasCompatibleActions']</code> | |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | <code>Adapters</code> | |
| [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | <code>RenderErrorHandlerFnType</code> | |
| [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | <code>RenderMode</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) &gt; [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md)
## IInterpreterRenderHandlers.hasCompatibleActions property
<b>Signature:</b>
```typescript
hasCompatibleActions?: (event: any) => Promise<boolean>;
```

View file

@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers
| [done](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.done.md) | <code>() =&gt; void</code> | Done increments the number of rendering successes |
| [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | <code>(event: any) =&gt; void</code> | |
| [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md) | <code>() =&gt; RenderMode</code> | |
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) | <code>(event: any) =&gt; Promise&lt;boolean&gt;</code> | |
| [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | <code>(fn: () =&gt; void) =&gt; void</code> | |
| [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | <code>() =&gt; void</code> | |
| [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | <code>PersistedState</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) &gt; [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) &gt; [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md)
## IInterpreterRenderHandlers.hasCompatibleActions property
<b>Signature:</b>
```typescript
hasCompatibleActions?: (event: any) => Promise<boolean>;
```

View file

@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers
| [done](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.done.md) | <code>() =&gt; void</code> | Done increments the number of rendering successes |
| [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | <code>(event: any) =&gt; void</code> | |
| [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md) | <code>() =&gt; RenderMode</code> | |
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) | <code>(event: any) =&gt; Promise&lt;boolean&gt;</code> | |
| [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | <code>(fn: () =&gt; void) =&gt; void</code> | |
| [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | <code>() =&gt; void</code> | |
| [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | <code>PersistedState</code> | |

View file

@ -26,6 +26,7 @@
| [Action](./kibana-plugin-plugins-ui_actions-public.action.md) | |
| [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) | |
| [ActionExecutionMeta](./kibana-plugin-plugins-ui_actions-public.actionexecutionmeta.md) | During action execution we can provide additional information, for example, trigger, that caused the action execution |
| [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) | |
| [Trigger](./kibana-plugin-plugins-ui_actions-public.trigger.md) | This is a convenience interface used to register a \*trigger\*.<code>Trigger</code> specifies a named anchor to which <code>Action</code> can be attached. When <code>Trigger</code> is being \*called\* it creates a <code>Context</code> object and passes it to the <code>execute</code> method of an <code>Action</code>.<!-- -->More than one action can be attached to a single trigger, in which case when trigger is \*called\* it first displays a context menu for user to pick a single action to execute. |
| [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) | |
| [UiActionsActionDefinition](./kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md) | A convenience interface used to register an action. |
@ -42,6 +43,8 @@
| [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md) | |
| [APPLY\_FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.apply_filter_trigger.md) | |
| [applyFilterTrigger](./kibana-plugin-plugins-ui_actions-public.applyfiltertrigger.md) | |
| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.row_click_trigger.md) | |
| [rowClickTrigger](./kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md) | |
| [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.select_range_trigger.md) | |
| [selectRangeTrigger](./kibana-plugin-plugins-ui_actions-public.selectrangetrigger.md) | |
| [VALUE\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.value_click_trigger.md) | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) &gt; [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.row_click_trigger.md)
## ROW\_CLICK\_TRIGGER variable
<b>Signature:</b>
```typescript
ROW_CLICK_TRIGGER = "ROW_CLICK_TRIGGER"
```

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) &gt; [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) &gt; [data](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md)
## RowClickContext.data property
<b>Signature:</b>
```typescript
data: {
rowIndex: number;
table: Datatable;
columns?: string[];
};
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) &gt; [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) &gt; [embeddable](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md)
## RowClickContext.embeddable property
<b>Signature:</b>
```typescript
embeddable?: IEmbeddable;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) &gt; [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md)
## RowClickContext interface
<b>Signature:</b>
```typescript
export interface RowClickContext
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [data](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md) | <code>{</code><br/><code> rowIndex: number;</code><br/><code> table: Datatable;</code><br/><code> columns?: string[];</code><br/><code> }</code> | |
| [embeddable](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md) | <code>IEmbeddable</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) &gt; [rowClickTrigger](./kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md)
## rowClickTrigger variable
<b>Signature:</b>
```typescript
rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'>
```

View file

@ -16,6 +16,7 @@ export interface TriggerContextMapping
| --- | --- | --- |
| [""](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md) | <code>TriggerContext</code> | |
| [FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.filter_trigger.md) | <code>ApplyGlobalFilterActionContext</code> | |
| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md) | <code>RowClickContext</code> | |
| [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.select_range_trigger.md) | <code>RangeSelectContext</code> | |
| [VALUE\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.value_click_trigger.md) | <code>ValueClickContext</code> | |
| [VISUALIZE\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md) | <code>VisualizeFieldContext</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) &gt; [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) &gt; [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md)
## TriggerContextMapping.ROW\_CLICK\_TRIGGER property
<b>Signature:</b>
```typescript
[ROW_CLICK_TRIGGER]: RowClickContext;
```

View file

@ -11,5 +11,5 @@
<b>Signature:</b>
```typescript
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
```

View file

@ -12,5 +12,5 @@
<b>Signature:</b>
```typescript
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
```

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
```

View file

@ -21,17 +21,17 @@ export declare class UiActionsService
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | <code>ActionRegistry</code> | |
| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, action: ActionDefinition&lt;TriggerContextMapping[T]&gt; &#124; Action&lt;TriggerContextMapping[T], &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;) =&gt; void</code> | <code>addTriggerAction</code> is similar to <code>attachAction</code> as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.<code>addTriggerAction</code> also infers better typing of the <code>action</code> argument. |
| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, actionId: string) =&gt; void</code> | |
| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;ROW_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, action: ActionDefinition&lt;TriggerContextMapping[T]&gt; &#124; Action&lt;TriggerContextMapping[T], &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;) =&gt; void</code> | <code>addTriggerAction</code> is similar to <code>attachAction</code> as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.<code>addTriggerAction</code> also infers better typing of the <code>action</code> argument. |
| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;ROW_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, actionId: string) =&gt; void</code> | |
| [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | <code>() =&gt; void</code> | Removes all registered triggers and actions. |
| [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | <code>(triggerId: TriggerId, actionId: string) =&gt; void</code> | |
| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, context: TriggerContext&lt;T&gt;) =&gt; Promise&lt;void&gt;</code> | |
| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;ROW_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, context: TriggerContext&lt;T&gt;) =&gt; Promise&lt;void&gt;</code> | |
| [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | <code>UiActionsExecutionService</code> | |
| [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | <code>() =&gt; UiActionsService</code> | "Fork" a separate instance of <code>UiActionsService</code> that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of <code>UiActionsService</code> are only available within this instance. |
| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <code>&lt;T extends ActionDefinition&lt;{}&gt;&gt;(id: string) =&gt; Action&lt;ActionContext&lt;T&gt;, &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;</code> | |
| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T) =&gt; TriggerContract&lt;T&gt;</code> | |
| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T) =&gt; Action&lt;TriggerContextMapping[T], &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;[]</code> | |
| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, context: TriggerContextMapping[T]) =&gt; Promise&lt;Action&lt;TriggerContextMapping[T], &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;[]&gt;</code> | |
| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;ROW_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T) =&gt; TriggerContract&lt;T&gt;</code> | |
| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;ROW_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T) =&gt; Action&lt;TriggerContextMapping[T], &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;[]</code> | |
| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <code>&lt;T extends &quot;&quot; &#124; &quot;SELECT_RANGE_TRIGGER&quot; &#124; &quot;VALUE_CLICK_TRIGGER&quot; &#124; &quot;ROW_CLICK_TRIGGER&quot; &#124; &quot;FILTER_TRIGGER&quot; &#124; &quot;VISUALIZE_FIELD_TRIGGER&quot; &#124; &quot;VISUALIZE_GEO_FIELD_TRIGGER&quot; &#124; &quot;CONTEXT_MENU_TRIGGER&quot; &#124; &quot;PANEL_BADGE_TRIGGER&quot; &#124; &quot;PANEL_NOTIFICATION_TRIGGER&quot;&gt;(triggerId: T, context: TriggerContextMapping[T]) =&gt; Promise&lt;Action&lt;TriggerContextMapping[T], &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;[]&gt;</code> | |
| [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | <code>(actionId: string) =&gt; boolean</code> | |
| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <code>&lt;A extends ActionDefinition&lt;{}&gt;&gt;(definition: A) =&gt; Action&lt;ActionContext&lt;A&gt;, &quot;&quot; &#124; &quot;ACTION_VISUALIZE_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_GEO_FIELD&quot; &#124; &quot;ACTION_VISUALIZE_LENS_FIELD&quot; &#124; &quot;ACTION_GLOBAL_APPLY_FILTER&quot; &#124; &quot;ACTION_SELECT_RANGE&quot; &#124; &quot;ACTION_VALUE_CLICK&quot; &#124; &quot;ACTION_CUSTOMIZE_PANEL&quot; &#124; &quot;ACTION_ADD_PANEL&quot; &#124; &quot;openInspector&quot; &#124; &quot;deletePanel&quot; &#124; &quot;editPanel&quot; &#124; &quot;togglePanel&quot; &#124; &quot;replacePanel&quot; &#124; &quot;clonePanel&quot; &#124; &quot;addToFromLibrary&quot; &#124; &quot;unlinkFromLibrary&quot; &#124; &quot;ACTION_LIBRARY_NOTIFICATION&quot; &#124; &quot;ACTION_EXPORT_CSV&quot;&gt;</code> | |
| [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | <code>(trigger: Trigger) =&gt; void</code> | |

View file

@ -233,7 +233,7 @@ image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigate
https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}}
----
+
The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice. In *URL preview*, `{{event.value}}` is substituted with a <<values-in-preview, dummy>> value.
The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice.
+
[role="screenshot"]
image:images/url_drilldown_url_template.png[URL template input]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -146,17 +146,7 @@ The URL drilldown template has three sources for variables:
* *Context* variables that change depending on where the drilldown is created and used. These variables are extracted from a context of a panel on a dashboard. For example, `{{context.panel.filters}}` gives access to filters that applied to the current panel.
* *Event* variables that depend on the trigger context. These variables are dynamically extracted from the interaction context when the drilldown is executed.
[[values-in-preview]]
A subtle but important difference between *context* and *event* variables is that *context* variables use real values in previews when creating a URL drilldown.
For example, `{{context.panel.filters}}` are previewed with the current filters that applied to a panel.
*Event* variables are extracted during drilldown execution from a user interaction with a panel (for example, from a pie slice that the user clicked on).
Because there is no user interaction with a panel in preview, there is no interaction context to use in a preview.
To work around this, {kib} provides a sample interaction that relies on a trigger.
So in a preview, you might notice that `{{event.value}}` is replaced with `{{event.value}}` instead of with a sample from your data.
Such previews can help you make sure that the structure of your URL template is valid.
However, to ensure that the configured URL drilldown works as expected with your data, you have to save the dashboard and test in the panel.
To ensure that the configured URL drilldown works as expected with your data, you have to save the dashboard and test in the panel.
You can access the full list of variables available for the current panel and selected trigger by clicking *Add variable* in the top-right corner of a URL template input.
[float]
@ -241,6 +231,22 @@ Note:
`{{event.value}}` is a shorthand for `{{event.points.[0].value}}` +
`{{event.key}}` is a shorthand for `{{event.points.[0].key}}`
| *Row click*
| event.rowIndex
| Number, representing the row that was clicked, starting from 0.
|
| event.values
| An array of all cell values for the raw on which the action will execute.
|
| event.keys
| An array of field names for each column.
|
| event.columnNames
| An array of column names.
| *Range selection*
| event.from +
event.to

View file

@ -83,10 +83,10 @@ pageLoadAssetSize:
transform: 41151
triggersActionsUi: 170145
uiActions: 95074
uiActionsEnhanced: 349799
uiActionsEnhanced: 313011
upgradeAssistant: 80966
uptime: 40825
urlDrilldown: 34174
urlDrilldown: 70674
urlForwarding: 32579
usageCollection: 39762
visDefaultEditor: 50178

View file

@ -54,6 +54,7 @@ export {
ErrorEmbeddable,
IContainer,
IEmbeddable,
isEmbeddable,
isErrorEmbeddable,
openAddPanelFlyout,
OutputSpec,
@ -70,6 +71,7 @@ export {
isSavedObjectEmbeddableInput,
isRangeSelectTriggerContext,
isValueClickTriggerContext,
isRowClickTriggerContext,
isContextMenuTriggerContext,
EmbeddableStateTransfer,
EmbeddableEditorState,

View file

@ -33,7 +33,7 @@ export { EmbeddableInput };
export interface EmbeddableOutput {
// Whether the embeddable is actively loading.
loading?: boolean;
// Whether the embeddable finshed loading with an error.
// Whether the embeddable finished loading with an error.
error?: EmbeddableError;
editUrl?: string;
editApp?: string;

View file

@ -17,6 +17,7 @@
* under the License.
*/
export { EmbeddableOutput, EmbeddableInput, IEmbeddable } from './i_embeddable';
export { isEmbeddable } from './is_embeddable';
export { Embeddable } from './embeddable';
export * from './embeddable_factory';
export * from './embeddable_factory_definition';

View file

@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IEmbeddable } from './i_embeddable';
export const isEmbeddable = (x: unknown): x is IEmbeddable => {
if (!x) return false;
if (typeof x !== 'object') return false;
if (typeof (x as IEmbeddable).id !== 'string') return false;
if (typeof (x as IEmbeddable).getInput !== 'function') return false;
if (typeof (x as IEmbeddable).supportedTriggers !== 'function') return false;
return true;
};

View file

@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
import { Datatable } from '../../../../expressions';
import { Trigger } from '../../../../ui_actions/public';
import { Trigger, RowClickContext } from '../../../../ui_actions/public';
import { IEmbeddable } from '..';
export interface EmbeddableContext {
@ -52,7 +52,8 @@ export interface RangeSelectContext<T extends IEmbeddable = IEmbeddable> {
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> =
| ValueClickContext<T>
| RangeSelectContext<T>;
| RangeSelectContext<T>
| RowClickContext;
export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER';
export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = {
@ -95,6 +96,11 @@ export const isRangeSelectTriggerContext = (
context: ChartActionContext
): context is RangeSelectContext => context.data && 'range' in context.data;
export const isRowClickTriggerContext = (context: ChartActionContext): context is RowClickContext =>
!!context.data &&
typeof context.data === 'object' &&
typeof (context as RowClickContext).data.rowIndex === 'number';
export const isContextMenuTriggerContext = (context: unknown): context is EmbeddableContext =>
!!context &&
typeof context === 'object' &&

View file

@ -176,10 +176,11 @@ export class AttributeService<SavedObjectAttributes extends {
wrapAttributes(newAttributes: SavedObjectAttributes, useRefType: boolean, input?: ValType | RefType): Promise<Omit<ValType | RefType, 'id'>>;
}
// Warning: (ae-forgotten-export) The symbol "RowClickContext" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ChartActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T>;
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T> | RowClickContext;
// Warning: (ae-missing-release-tag) "Container" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -726,6 +727,11 @@ export interface IEmbeddable<I extends EmbeddableInput = EmbeddableInput, O exte
// @public (undocumented)
export const isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext;
// Warning: (ae-missing-release-tag) "isEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const isEmbeddable: (x: unknown) => x is IEmbeddable<import("./i_embeddable").EmbeddableInput, import("./i_embeddable").EmbeddableOutput>;
// Warning: (ae-missing-release-tag) "isErrorEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -741,6 +747,11 @@ export const isRangeSelectTriggerContext: (context: ChartActionContext) => conte
// @public (undocumented)
export function isReferenceOrValueEmbeddable(incoming: unknown): incoming is ReferenceOrValueEmbeddable;
// Warning: (ae-missing-release-tag) "isRowClickTriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const isRowClickTriggerContext: (context: ChartActionContext) => context is RowClickContext;
// Warning: (ae-missing-release-tag) "isSavedObjectEmbeddableInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)

View file

@ -82,6 +82,7 @@ export interface IInterpreterRenderHandlers {
reload: () => void;
update: (params: any) => void;
event: (event: any) => void;
hasCompatibleActions?: (event: any) => Promise<boolean>;
getRenderMode: () => RenderMode;
uiState?: PersistedState;
}

View file

@ -64,6 +64,7 @@ export class ExpressionLoader {
this.renderHandler = new ExpressionRenderHandler(element, {
onRenderError: params && params.onRenderError,
renderMode: params?.renderMode,
hasCompatibleActions: params?.hasCompatibleActions,
});
this.render$ = this.renderHandler.render$;
this.update$ = this.renderHandler.update$;

View file

@ -532,7 +532,7 @@ export interface ExpressionRenderError extends Error {
// @public (undocumented)
export class ExpressionRenderHandler {
// Warning: (ae-forgotten-export) The symbol "ExpressionRenderHandlerParams" needs to be exported by the entry point index.d.ts
constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial<ExpressionRenderHandlerParams>);
constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
// (undocumented)
destroy: () => void;
// (undocumented)
@ -544,7 +544,7 @@ export class ExpressionRenderHandler {
// (undocumented)
render$: Observable<number>;
// (undocumented)
render: (data: any, uiState?: any) => Promise<void>;
render: (value: any, uiState?: any) => Promise<void>;
// Warning: (ae-forgotten-export) The symbol "UpdateValue" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -888,6 +888,8 @@ export interface IExpressionLoaderParams {
// (undocumented)
disableCaching?: boolean;
// (undocumented)
hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
// (undocumented)
inspectorAdapters?: Adapters;
// Warning: (ae-forgotten-export) The symbol "RenderErrorHandlerFnType" needs to be exported by the entry point index.d.ts
//
@ -917,6 +919,8 @@ export interface IInterpreterRenderHandlers {
// (undocumented)
getRenderMode: () => RenderMode;
// (undocumented)
hasCompatibleActions?: (event: any) => Promise<boolean>;
// (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)
reload: () => void;

View file

@ -126,6 +126,31 @@ describe('ExpressionRenderHandler', () => {
expect(getHandledError()!.message).toEqual('renderer error');
});
it('should pass through provided "hasCompatibleActions" to the expression renderer', async () => {
const hasCompatibleActions = jest.fn();
(getRenderersRegistry as jest.Mock).mockReturnValueOnce({ get: () => true });
(getRenderersRegistry as jest.Mock).mockReturnValueOnce({
get: () => ({
render: (domNode: HTMLElement, config: unknown, handlers: IInterpreterRenderHandlers) => {
handlers.hasCompatibleActions!({
foo: 'bar',
});
},
}),
});
const expressionRenderHandler = new ExpressionRenderHandler(element, {
onRenderError: mockMockErrorRenderFunction,
hasCompatibleActions,
});
expect(hasCompatibleActions).toHaveBeenCalledTimes(0);
await expressionRenderHandler.render({ type: 'render', as: 'something' });
expect(hasCompatibleActions).toHaveBeenCalledTimes(1);
expect(hasCompatibleActions.mock.calls[0][0]).toEqual({
foo: 'bar',
});
});
it('sends a next observable once rendering is complete', () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
expect.assertions(1);

View file

@ -29,8 +29,9 @@ import { getRenderersRegistry } from './services';
export type IExpressionRendererExtraHandlers = Record<string, any>;
export interface ExpressionRenderHandlerParams {
onRenderError: RenderErrorHandlerFnType;
renderMode: RenderMode;
onRenderError?: RenderErrorHandlerFnType;
renderMode?: RenderMode;
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
}
export interface ExpressionRendererEvent {
@ -59,7 +60,11 @@ export class ExpressionRenderHandler {
constructor(
element: HTMLElement,
{ onRenderError, renderMode }: Partial<ExpressionRenderHandlerParams> = {}
{
onRenderError,
renderMode,
hasCompatibleActions = async () => false,
}: ExpressionRenderHandlerParams = {}
) {
this.element = element;
@ -96,17 +101,18 @@ export class ExpressionRenderHandler {
getRenderMode: () => {
return renderMode || 'display';
},
hasCompatibleActions,
};
}
render = async (data: any, uiState: any = {}) => {
if (!data || typeof data !== 'object') {
render = async (value: any, uiState: any = {}) => {
if (!value || typeof value !== 'object') {
return this.handleRenderError(new Error('invalid data provided to the expression renderer'));
}
if (data.type !== 'render' || !data.as) {
if (data.type === 'error') {
return this.handleRenderError(data.error);
if (value.type !== 'render' || !value.as) {
if (value.type === 'error') {
return this.handleRenderError(value.error);
} else {
return this.handleRenderError(
new Error('invalid data provided to the expression renderer')
@ -114,15 +120,15 @@ export class ExpressionRenderHandler {
}
}
if (!getRenderersRegistry().get(data.as)) {
return this.handleRenderError(new Error(`invalid renderer id '${data.as}'`));
if (!getRenderersRegistry().get(value.as)) {
return this.handleRenderError(new Error(`invalid renderer id '${value.as}'`));
}
try {
// Rendering is asynchronous, completed by handlers.done()
await getRenderersRegistry()
.get(data.as)!
.render(this.element, data.value, {
.get(value.as)!
.render(this.element, value.value, {
...this.handlers,
uiState,
} as any);
@ -152,7 +158,7 @@ export class ExpressionRenderHandler {
export function render(
element: HTMLElement,
data: any,
options?: Partial<ExpressionRenderHandlerParams>
options?: ExpressionRenderHandlerParams
): ExpressionRenderHandler {
const handler = new ExpressionRenderHandler(element, options);
handler.render(data);

View file

@ -25,6 +25,7 @@ import {
SerializableState,
RenderMode,
} from '../../common';
import { ExpressionRenderHandlerParams } from '../render';
/**
* @deprecated
@ -56,6 +57,7 @@ export interface IExpressionLoaderParams {
onRenderError?: RenderErrorHandlerFnType;
searchSessionId?: string;
renderMode?: RenderMode;
hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
}
export interface ExpressionRenderError extends Error {

View file

@ -736,6 +736,8 @@ export interface IInterpreterRenderHandlers {
// (undocumented)
getRenderMode: () => RenderMode;
// (undocumented)
hasCompatibleActions?: (event: any) => Promise<boolean>;
// (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)
reload: () => void;

View file

@ -50,6 +50,9 @@ export {
visualizeFieldTrigger,
VISUALIZE_GEO_FIELD_TRIGGER,
visualizeGeoFieldTrigger,
ROW_CLICK_TRIGGER,
rowClickTrigger,
RowClickContext,
} from './triggers';
export {
TriggerContextMapping,

View file

@ -23,6 +23,7 @@ import { UiActionsService } from './service';
import {
selectRangeTrigger,
valueClickTrigger,
rowClickTrigger,
applyFilterTrigger,
visualizeFieldTrigger,
visualizeGeoFieldTrigger,
@ -48,6 +49,7 @@ export class UiActionsPlugin implements Plugin<UiActionsSetup, UiActionsStart> {
public setup(core: CoreSetup): UiActionsSetup {
this.service.registerTrigger(selectRangeTrigger);
this.service.registerTrigger(valueClickTrigger);
this.service.registerTrigger(rowClickTrigger);
this.service.registerTrigger(applyFilterTrigger);
this.service.registerTrigger(visualizeFieldTrigger);
this.service.registerTrigger(visualizeGeoFieldTrigger);

View file

@ -133,6 +133,32 @@ export class IncompatibleActionError extends Error {
// @public (undocumented)
export function plugin(initializerContext: PluginInitializerContext): UiActionsPlugin;
// Warning: (ae-missing-release-tag) "ROW_CLICK_TRIGGER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const ROW_CLICK_TRIGGER = "ROW_CLICK_TRIGGER";
// Warning: (ae-missing-release-tag) "RowClickContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface RowClickContext {
// (undocumented)
data: {
rowIndex: number;
table: Datatable;
columns?: string[];
};
// Warning: (ae-forgotten-export) The symbol "IEmbeddable" needs to be exported by the entry point index.d.ts
//
// (undocumented)
embeddable?: IEmbeddable;
}
// Warning: (ae-missing-release-tag) "rowClickTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'>;
// Warning: (ae-missing-release-tag) "SELECT_RANGE_TRIGGER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -170,6 +196,8 @@ export interface TriggerContextMapping {
//
// (undocumented)
[APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext;
// (undocumented)
[ROW_CLICK_TRIGGER]: RowClickContext;
// Warning: (ae-forgotten-export) The symbol "RangeSelectContext" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -234,14 +262,14 @@ export class UiActionsService {
//
// (undocumented)
protected readonly actions: ActionRegistry;
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: UiActionsActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: UiActionsActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
// (undocumented)
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
readonly clear: () => void;
// (undocumented)
readonly detachAction: (triggerId: TriggerId, actionId: string) => void;
// @deprecated (undocumented)
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
// Warning: (ae-forgotten-export) The symbol "UiActionsExecutionService" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -252,11 +280,11 @@ export class UiActionsService {
// Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts
//
// (undocumented)
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
// (undocumented)
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
// (undocumented)
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
// (undocumented)
readonly hasAction: (actionId: string) => boolean;
// Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts
@ -341,6 +369,10 @@ export const visualizeFieldTrigger: Trigger<'VISUALIZE_FIELD_TRIGGER'>;
export const visualizeGeoFieldTrigger: Trigger<'VISUALIZE_GEO_FIELD_TRIGGER'>;
// Warnings were encountered during analysis:
//
// src/plugins/ui_actions/public/triggers/row_click_trigger.ts:45:5 - (ae-forgotten-export) The symbol "Datatable" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
```

View file

@ -29,6 +29,7 @@ interface ExecuteActionTask {
context: BaseContext;
trigger: Trigger;
defer: Defer<void>;
alwaysShowPopup?: boolean;
}
export class UiActionsExecutionService {
@ -37,21 +38,25 @@ export class UiActionsExecutionService {
constructor() {}
async execute({
action,
context,
trigger,
}: {
action: Action<BaseContext>;
context: BaseContext;
trigger: Trigger;
}): Promise<void> {
async execute(
{
action,
context,
trigger,
}: {
action: Action<BaseContext>;
context: BaseContext;
trigger: Trigger;
},
alwaysShowPopup?: boolean
): Promise<void> {
const shouldBatch = !(await action.shouldAutoExecute?.({ ...context, trigger })) ?? false;
const task: ExecuteActionTask = {
action,
context,
trigger,
defer: createDefer(),
alwaysShowPopup: !!alwaysShowPopup,
};
if (shouldBatch) {
@ -84,11 +89,23 @@ export class UiActionsExecutionService {
setTimeout(() => {
if (this.pendingTasks.size === 0) {
const tasks = uniqBy(this.batchingQueue, (t) => t.action.id);
if (tasks.length === 1) {
this.executeSingleTask(tasks[0]);
}
if (tasks.length > 1) {
this.executeMultipleActions(tasks);
if (tasks.length > 0) {
let alwaysShowPopup = false;
for (const task of tasks) {
if (task.alwaysShowPopup) {
alwaysShowPopup = true;
break;
}
}
if (alwaysShowPopup) {
this.showActionPopupMenu(tasks);
} else {
if (tasks.length === 1) {
this.executeSingleTask(tasks[0]);
} else if (tasks.length > 1) {
this.showActionPopupMenu(tasks);
}
}
}
this.batchingQueue.splice(0, this.batchingQueue.length);
@ -108,7 +125,7 @@ export class UiActionsExecutionService {
}
}
private async executeMultipleActions(tasks: ExecuteActionTask[]) {
private async showActionPopupMenu(tasks: ExecuteActionTask[]) {
const panels = await buildContextMenuForActions({
actions: tasks.map(({ action, context, trigger }) => ({
action,

View file

@ -143,7 +143,32 @@ test('shows a context menu when more than one action is mapped to a trigger', as
const start = doStart();
const context = {};
await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context);
await start.getTrigger('MY-TRIGGER' as TriggerId)!.exec(context);
jest.runAllTimers();
await waitFor(() => {
expect(executeFn).toBeCalledTimes(0);
expect(openContextMenu).toHaveBeenCalledTimes(1);
});
});
test('shows a context menu when there is only one action mapped to a trigger and "alwaysShowPopup" is set', async () => {
const { setup, doStart } = uiActions;
const trigger: Trigger = {
id: 'MY-TRIGGER' as TriggerId,
title: 'My trigger',
};
const action1 = createTestAction('test1', () => true);
setup.registerTrigger(trigger);
setup.addTriggerAction(trigger.id, action1);
expect(openContextMenu).toHaveBeenCalledTimes(0);
const start = doStart();
const context = {};
await start.getTrigger('MY-TRIGGER' as TriggerId)!.exec(context, true);
jest.runAllTimers();

View file

@ -22,6 +22,7 @@ export * from './trigger_contract';
export * from './trigger_internal';
export * from './select_range_trigger';
export * from './value_click_trigger';
export * from './row_click_trigger';
export * from './apply_filter_trigger';
export * from './visualize_field_trigger';
export * from './visualize_geo_field_trigger';

View file

@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { IEmbeddable } from '../../../embeddable/public';
import { Trigger } from '.';
import { Datatable } from '../../../expressions';
export const ROW_CLICK_TRIGGER = 'ROW_CLICK_TRIGGER';
export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'> = {
id: ROW_CLICK_TRIGGER,
title: i18n.translate('uiActions.triggers.rowClickTitle', {
defaultMessage: 'Table row click',
}),
description: i18n.translate('uiActions.triggers.rowClickkDescription', {
defaultMessage: 'A click on a table row',
}),
};
export interface RowClickContext {
embeddable?: IEmbeddable;
data: {
/**
* Row index, starting from 0, where user clicked.
*/
rowIndex: number;
table: Datatable;
/**
* Sorted list column IDs that were visible to the user. Useful when only
* a subset of datatable columns should be used.
*/
columns?: string[];
};
}

View file

@ -49,7 +49,7 @@ export class TriggerContract<T extends TriggerId> {
/**
* Use this method to execute action attached to this trigger.
*/
public readonly exec = async (context: TriggerContextMapping[T]) => {
await this.internal.execute(context);
public readonly exec = async (context: TriggerContextMapping[T], alwaysShowPopup?: boolean) => {
await this.internal.execute(context, alwaysShowPopup);
};
}

View file

@ -31,17 +31,20 @@ export class TriggerInternal<T extends TriggerId> {
constructor(public readonly service: UiActionsService, public readonly trigger: Trigger<T>) {}
public async execute(context: TriggerContextMapping[T]) {
public async execute(context: TriggerContextMapping[T], alwaysShowPopup?: boolean) {
const triggerId = this.trigger.id;
const actions = await this.service.getTriggerCompatibleActions!(triggerId, context);
await Promise.all([
actions.map((action) =>
this.service.executionService.execute({
action,
context,
trigger: this.trigger,
})
this.service.executionService.execute(
{
action,
context,
trigger: this.trigger,
},
alwaysShowPopup
)
),
]);
}

View file

@ -22,10 +22,12 @@ import { TriggerInternal } from './triggers/trigger_internal';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
ROW_CLICK_TRIGGER,
APPLY_FILTER_TRIGGER,
VISUALIZE_FIELD_TRIGGER,
VISUALIZE_GEO_FIELD_TRIGGER,
DEFAULT_TRIGGER,
RowClickContext,
} from './triggers';
import type { RangeSelectContext, ValueClickContext } from '../../embeddable/public';
import type { ApplyGlobalFilterActionContext } from '../../data/public';
@ -49,6 +51,7 @@ export interface TriggerContextMapping {
[DEFAULT_TRIGGER]: TriggerContext;
[SELECT_RANGE_TRIGGER]: RangeSelectContext;
[VALUE_CLICK_TRIGGER]: ValueClickContext;
[ROW_CLICK_TRIGGER]: RowClickContext;
[APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext;
[VISUALIZE_FIELD_TRIGGER]: VisualizeFieldContext;
[VISUALIZE_GEO_FIELD_TRIGGER]: VisualizeFieldContext;

View file

@ -21,16 +21,19 @@ import {
APPLY_FILTER_TRIGGER,
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
} from '../../../../plugins/ui_actions/public';
ROW_CLICK_TRIGGER,
} from '../../../ui_actions/public';
export interface VisEventToTrigger {
['applyFilter']: typeof APPLY_FILTER_TRIGGER;
['brush']: typeof SELECT_RANGE_TRIGGER;
['filter']: typeof VALUE_CLICK_TRIGGER;
['tableRowContextMenuClick']: typeof ROW_CLICK_TRIGGER;
}
export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = {
applyFilter: APPLY_FILTER_TRIGGER,
brush: SELECT_RANGE_TRIGGER,
filter: VALUE_CLICK_TRIGGER,
tableRowContextMenuClick: ROW_CLICK_TRIGGER,
};

View file

@ -0,0 +1,173 @@
/*
* 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 { DatatableColumnType } from '../../../../../../../src/plugins/expressions/common';
import {
Embeddable,
EmbeddableInput,
EmbeddableOutput,
} from '../../../../../../../src/plugins/embeddable/public';
export const createPoint = ({
field,
value,
}: {
field: string;
value: string | null | number | boolean;
}) => ({
table: {
columns: [
{
name: field,
id: '1-1',
meta: {
type: 'date' as DatatableColumnType,
field,
source: 'esaggs',
sourceParams: {
type: 'histogram',
indexPatternId: 'logstash-*',
interval: 30,
otherBucket: true,
},
},
},
],
rows: [
{
'1-1': '2048',
},
],
},
column: 0,
row: 0,
value,
});
export const rowClickData = {
rowIndex: 1,
table: {
type: 'datatable',
rows: [
{
'6ced5344-2596-4545-b626-8b449924e2d4': 'IT',
'6890e417-c5f1-4565-a45c-92f55380e14c': '0',
'93b8ef16-2483-45b8-ad27-6cc1f790578b': 13,
'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 0,
'e0719f1a-04fb-4036-a63c-c25deac3f011': 7,
},
{
'6ced5344-2596-4545-b626-8b449924e2d4': 'IT',
'6890e417-c5f1-4565-a45c-92f55380e14c': '2.25',
'93b8ef16-2483-45b8-ad27-6cc1f790578b': 3,
'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 0,
'e0719f1a-04fb-4036-a63c-c25deac3f011': 2,
},
{
'6ced5344-2596-4545-b626-8b449924e2d4': 'IT',
'6890e417-c5f1-4565-a45c-92f55380e14c': '0.020939215995129826',
'93b8ef16-2483-45b8-ad27-6cc1f790578b': 2,
'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 12.490584373474121,
'e0719f1a-04fb-4036-a63c-c25deac3f011': 1,
},
],
columns: [
{
id: '6ced5344-2596-4545-b626-8b449924e2d4',
name: 'Top values of DestCountry',
meta: {
type: 'string',
field: 'DestCountry',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: '(missing value)',
},
},
source: 'esaggs',
},
},
{
id: '6890e417-c5f1-4565-a45c-92f55380e14c',
name: 'Top values of FlightTimeHour',
meta: {
type: 'string',
field: 'FlightTimeHour',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: '(missing value)',
},
},
source: 'esaggs',
},
},
{
id: '93b8ef16-2483-45b8-ad27-6cc1f790578b',
name: 'Count of records',
meta: {
type: 'number',
index: 'kibana_sample_data_flights',
params: {
id: 'number',
},
},
},
{
id: 'b0c5dcc2-4012-4d7e-b983-0e089badc43c',
name: 'Average of DistanceMiles',
meta: {
type: 'number',
field: 'DistanceMiles',
index: 'kibana_sample_data_flights',
params: {
id: 'number',
},
},
},
{
id: 'e0719f1a-04fb-4036-a63c-c25deac3f011',
name: 'Unique count of OriginAirportID',
meta: {
type: 'string',
field: 'OriginAirportID',
index: 'kibana_sample_data_flights',
params: {
id: 'number',
},
},
},
],
},
columns: [
'6ced5344-2596-4545-b626-8b449924e2d4',
'6890e417-c5f1-4565-a45c-92f55380e14c',
'93b8ef16-2483-45b8-ad27-6cc1f790578b',
'b0c5dcc2-4012-4d7e-b983-0e089badc43c',
'e0719f1a-04fb-4036-a63c-c25deac3f011',
],
};
interface TestInput extends EmbeddableInput {
savedObjectId?: string;
}
interface TestOutput extends EmbeddableOutput {
indexPatterns?: Array<{ id: string }>;
}
export class TestEmbeddable extends Embeddable<TestInput, TestOutput> {
type = 'test';
destroy() {}
reload() {}
}

View file

@ -7,6 +7,11 @@
import { UrlDrilldown, ActionContext, Config } from './url_drilldown';
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables';
import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common';
import { createPoint, rowClickData, TestEmbeddable } from './test/data';
import {
VALUE_CLICK_TRIGGER,
ROW_CLICK_TRIGGER,
} from '../../../../../../src/plugins/ui_actions/public';
const mockDataPoints = [
{
@ -99,7 +104,8 @@ describe('UrlDrilldown', () => {
embeddable: mockEmbeddable,
};
await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(true);
const result = urlDrilldown.isCompatible(config, context);
await expect(result).resolves.toBe(true);
});
test('not compatible if url is invalid', async () => {
@ -168,4 +174,199 @@ describe('UrlDrilldown', () => {
expect(mockNavigateToUrl).not.toBeCalled();
});
});
describe('variables', () => {
const embeddable1 = new TestEmbeddable(
{
id: 'test',
title: 'The Title',
savedObjectId: 'SAVED_OBJECT_IDxx',
},
{
indexPatterns: [{ id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }],
}
);
const data: any = {
data: [
createPoint({ field: 'field0', value: 'value0' }),
createPoint({ field: 'field1', value: 'value1' }),
createPoint({ field: 'field2', value: 'value2' }),
],
};
const embeddable2 = new TestEmbeddable(
{
id: 'the-id',
query: {
language: 'C++',
query: 'std::cout << 123;',
},
timeRange: {
from: 'FROM',
to: 'TO',
},
filters: [
{
meta: {
alias: 'asdf',
disabled: false,
negate: false,
},
},
],
savedObjectId: 'SAVED_OBJECT_ID',
},
{
title: 'The Title',
indexPatterns: [
{ id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' },
{ id: 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' },
],
}
);
describe('getRuntimeVariables()', () => {
test('builds runtime variables for VALUE_CLICK_TRIGGER trigger', () => {
const variables = urlDrilldown.getRuntimeVariables({
embeddable: embeddable1,
data,
});
expect(variables).toMatchObject({
kibanaUrl: 'http://localhost:5601/',
context: {
panel: {
id: 'test',
title: 'The Title',
savedObjectId: 'SAVED_OBJECT_IDxx',
indexPatternId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
},
},
event: {
key: 'field0',
value: 'value0',
negate: false,
points: [
{
value: 'value0',
key: 'field0',
},
{
value: 'value1',
key: 'field1',
},
{
value: 'value2',
key: 'field2',
},
],
},
});
});
test('builds runtime variables for ROW_CLICK_TRIGGER trigger', () => {
const variables = urlDrilldown.getRuntimeVariables({
embeddable: embeddable2,
data: rowClickData as any,
});
expect(variables).toMatchObject({
kibanaUrl: 'http://localhost:5601/',
context: {
panel: {
id: 'the-id',
title: 'The Title',
savedObjectId: 'SAVED_OBJECT_ID',
query: {
language: 'C++',
query: 'std::cout << 123;',
},
timeRange: {
from: 'FROM',
to: 'TO',
},
filters: [
{
meta: {
alias: 'asdf',
disabled: false,
negate: false,
},
},
],
},
},
event: {
rowIndex: 1,
values: ['IT', '2.25', 3, 0, 2],
keys: ['DestCountry', 'FlightTimeHour', '', 'DistanceMiles', 'OriginAirportID'],
columnNames: [
'Top values of DestCountry',
'Top values of FlightTimeHour',
'Count of records',
'Average of DistanceMiles',
'Unique count of OriginAirportID',
],
},
});
});
});
describe('getVariableList()', () => {
test('builds variable list for VALUE_CLICK_TRIGGER trigger', () => {
const list = urlDrilldown.getVariableList({
triggers: [VALUE_CLICK_TRIGGER],
embeddable: embeddable1,
});
const expectedList = [
'event.key',
'event.value',
'event.negate',
'event.points',
'context.panel.id',
'context.panel.title',
'context.panel.indexPatternId',
'context.panel.savedObjectId',
'kibanaUrl',
];
for (const expectedItem of expectedList) {
expect(list.includes(expectedItem)).toBe(true);
}
});
test('builds variable list for ROW_CLICK_TRIGGER trigger', () => {
const list = urlDrilldown.getVariableList({
triggers: [ROW_CLICK_TRIGGER],
embeddable: embeddable2,
});
const expectedList = [
'event.columnNames',
'event.keys',
'event.rowIndex',
'event.values',
'context.panel.id',
'context.panel.title',
'context.panel.filters',
'context.panel.query.language',
'context.panel.query.query',
'context.panel.indexPatternIds',
'context.panel.savedObjectId',
'context.panel.timeRange.from',
'context.panel.timeRange.to',
'kibanaUrl',
];
for (const expectedItem of expectedList) {
expect(list.includes(expectedItem)).toBe(true);
}
});
});
});
});

View file

@ -5,6 +5,7 @@
*/
import React from 'react';
import { getFlattenedObject } from '@kbn/std';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import {
ChartActionContext,
@ -13,6 +14,7 @@ import {
} from '../../../../../../src/plugins/embeddable/public';
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
import {
ROW_CLICK_TRIGGER,
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
} from '../../../../../../src/plugins/ui_actions/public';
@ -22,11 +24,10 @@ import {
UrlDrilldownConfig,
UrlDrilldownCollectConfig,
urlDrilldownValidateUrlTemplate,
urlDrilldownBuildScope,
urlDrilldownCompileUrl,
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
} from '../../../../ui_actions_enhanced/public';
import { getContextScope, getEventScope, getMockEventScope } from './url_drilldown_scope';
import { getPanelVariables, getEventScope, getEventVariableList } from './url_drilldown_scope';
import { txtUrlDrilldownDisplayName } from './i18n';
interface UrlDrilldownDeps {
@ -39,9 +40,11 @@ interface UrlDrilldownDeps {
export type ActionContext = ChartActionContext;
export type Config = UrlDrilldownConfig;
export type UrlTrigger =
| typeof CONTEXT_MENU_TRIGGER
| typeof VALUE_CLICK_TRIGGER
| typeof SELECT_RANGE_TRIGGER;
| typeof SELECT_RANGE_TRIGGER
| typeof ROW_CLICK_TRIGGER
| typeof CONTEXT_MENU_TRIGGER;
export interface ActionFactoryContext extends BaseActionFactoryContext<UrlTrigger> {
embeddable?: IEmbeddable;
}
@ -65,7 +68,7 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
public readonly euiIcon = 'link';
supportedTriggers(): UrlTrigger[] {
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, CONTEXT_MENU_TRIGGER];
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, ROW_CLICK_TRIGGER, CONTEXT_MENU_TRIGGER];
}
private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({
@ -74,12 +77,12 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
context,
}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const scope = React.useMemo(() => this.buildEditorScope(context), [context]);
const variables = React.useMemo(() => this.getVariableList(context), [context]);
return (
<UrlDrilldownCollectConfig
variables={variables}
config={config}
onConfig={onConfig}
scope={scope}
syntaxHelpDocsLink={this.deps.getSyntaxHelpDocsLink()}
variablesHelpDocsLink={this.deps.getVariablesHelpDocsLink()}
/>
@ -93,19 +96,13 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
openInNewTab: false,
});
public readonly isConfigValid = (
config: Config,
context: ActionFactoryContext
): config is Config => {
const { isValid } = urlDrilldownValidateUrlTemplate(config.url, this.buildEditorScope(context));
return isValid;
public readonly isConfigValid = (config: Config): config is Config => {
return !!config.url.template;
};
public readonly isCompatible = async (config: Config, context: ActionContext) => {
const { isValid, error } = urlDrilldownValidateUrlTemplate(
config.url,
await this.buildRuntimeScope(context)
);
const scope = this.getRuntimeVariables(context);
const { isValid, error } = urlDrilldownValidateUrlTemplate(config.url, scope);
if (!isValid) {
// eslint-disable-next-line no-console
@ -117,11 +114,13 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
return Promise.resolve(isValid);
};
public readonly getHref = async (config: Config, context: ActionContext) =>
urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context));
public readonly getHref = async (config: Config, context: ActionContext) => {
const scope = this.getRuntimeVariables(context);
return urlDrilldownCompileUrl(config.url.template, scope);
};
public readonly execute = async (config: Config, context: ActionContext) => {
const url = urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context));
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
if (config.openInNewTab) {
window.open(url, '_blank', 'noopener');
} else {
@ -129,19 +128,23 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
}
};
private buildEditorScope = (context: ActionFactoryContext) => {
return urlDrilldownBuildScope({
globalScope: this.deps.getGlobalScope(),
contextScope: getContextScope(context),
eventScope: getMockEventScope(context.triggers),
});
public readonly getRuntimeVariables = (context: ActionContext) => {
return {
...this.deps.getGlobalScope(),
context: {
panel: getPanelVariables(context),
},
event: getEventScope(context),
};
};
private buildRuntimeScope = (context: ActionContext) => {
return urlDrilldownBuildScope({
globalScope: this.deps.getGlobalScope(),
contextScope: getContextScope(context),
eventScope: getEventScope(context),
});
public readonly getVariableList = (context: ActionFactoryContext): string[] => {
const eventVariables = getEventVariableList(context);
const contextVariables = Object.keys(getFlattenedObject(getPanelVariables(context))).map(
(key) => 'context.panel.' + key
);
const globalVariables = Object.keys(getFlattenedObject(this.deps.getGlobalScope()));
return [...eventVariables.sort(), ...contextVariables.sort(), ...globalVariables.sort()];
};
}

View file

@ -6,46 +6,15 @@
import {
getEventScope,
getMockEventScope,
ValueClickTriggerEventScope,
getEventVariableList,
getPanelVariables,
} from './url_drilldown_scope';
import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common';
const createPoint = ({
field,
value,
}: {
field: string;
value: string | null | number | boolean;
}) => ({
table: {
columns: [
{
name: field,
id: '1-1',
meta: {
type: 'date' as DatatableColumnType,
field,
source: 'esaggs',
sourceParams: {
type: 'histogram',
indexPatternId: 'logstash-*',
interval: 30,
otherBucket: true,
},
},
},
],
rows: [
{
'1-1': '2048',
},
],
},
column: 0,
row: 0,
value,
});
import {
RowClickContext,
ROW_CLICK_TRIGGER,
} from '../../../../../../src/plugins/ui_actions/public';
import { createPoint, rowClickData, TestEmbeddable } from './test/data';
describe('VALUE_CLICK_TRIGGER', () => {
describe('supports `points[]`', () => {
@ -80,33 +49,6 @@ describe('VALUE_CLICK_TRIGGER', () => {
]
`);
});
test('getMockEventScope()', () => {
const mockEventScope = getMockEventScope([
'VALUE_CLICK_TRIGGER',
]) as ValueClickTriggerEventScope;
expect(mockEventScope.points.length).toBeGreaterThan(3);
expect(mockEventScope.points).toMatchInlineSnapshot(`
Array [
Object {
"key": "event.points.0.key",
"value": "event.points.0.value",
},
Object {
"key": "event.points.1.key",
"value": "event.points.1.value",
},
Object {
"key": "event.points.2.key",
"value": "event.points.2.value",
},
Object {
"key": "event.points.3.key",
"value": "event.points.3.value",
},
]
`);
});
});
describe('handles undefined, null or missing values', () => {
@ -131,11 +73,221 @@ describe('VALUE_CLICK_TRIGGER', () => {
});
});
describe('CONTEXT_MENU_TRIGGER', () => {
test('getMockEventScope() results in empty scope', () => {
const mockEventScope = getMockEventScope([
'CONTEXT_MENU_TRIGGER',
]) as ValueClickTriggerEventScope;
expect(mockEventScope).toEqual({});
describe('ROW_CLICK_TRIGGER', () => {
test('getEventVariableList() returns correct list of runtime variables', () => {
const vars = getEventVariableList({
triggers: [ROW_CLICK_TRIGGER],
});
expect(vars).toEqual(['event.rowIndex', 'event.values', 'event.keys', 'event.columnNames']);
});
test('getEventScope() returns correct variables for row click trigger', () => {
const context = ({
embeddable: {},
data: rowClickData as any,
} as unknown) as RowClickContext;
const res = getEventScope(context);
expect(res).toEqual({
rowIndex: 1,
values: ['IT', '2.25', 3, 0, 2],
keys: ['DestCountry', 'FlightTimeHour', '', 'DistanceMiles', 'OriginAirportID'],
columnNames: [
'Top values of DestCountry',
'Top values of FlightTimeHour',
'Count of records',
'Average of DistanceMiles',
'Unique count of OriginAirportID',
],
});
});
});
describe('getPanelVariables()', () => {
test('returns only ID for empty embeddable', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
},
{}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
});
});
test('returns title as specified in input', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
title: 'title1',
},
{}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
title: 'title1',
});
});
test('returns output title if input and output titles are specified', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
title: 'title1',
},
{
title: 'title2',
}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
title: 'title2',
});
});
test('returns title from output if title in input is missing', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
},
{
title: 'title2',
}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
title: 'title2',
});
});
test('returns saved object ID from output', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
savedObjectId: '5678',
},
{
savedObjectId: '1234',
}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
savedObjectId: '1234',
});
});
test('returns saved object ID from input if it is not set on output', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
savedObjectId: '5678',
},
{}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
savedObjectId: '5678',
});
});
test('returns query, timeRange and filters from input', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
query: {
language: 'C++',
query: 'std::cout << 123;',
},
timeRange: {
from: 'FROM',
to: 'TO',
},
filters: [
{
meta: {
alias: 'asdf',
disabled: false,
negate: false,
},
},
],
},
{}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
query: {
language: 'C++',
query: 'std::cout << 123;',
},
timeRange: {
from: 'FROM',
to: 'TO',
},
filters: [
{
meta: {
alias: 'asdf',
disabled: false,
negate: false,
},
},
],
});
});
test('returns a single index pattern from output', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
},
{
indexPatterns: [{ id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }],
}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
indexPatternId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
});
});
test('returns multiple index patterns from output', () => {
const embeddable = new TestEmbeddable(
{
id: 'test',
},
{
indexPatterns: [
{ id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' },
{ id: 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' },
],
}
);
const vars = getPanelVariables({ embeddable });
expect(vars).toEqual({
id: 'test',
indexPatternIds: [
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy',
],
});
});
});

View file

@ -14,48 +14,54 @@ import {
IEmbeddable,
isRangeSelectTriggerContext,
isValueClickTriggerContext,
isRowClickTriggerContext,
isContextMenuTriggerContext,
RangeSelectContext,
ValueClickContext,
EmbeddableOutput,
} from '../../../../../../src/plugins/embeddable/public';
import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown';
import type { ActionContext, ActionFactoryContext } from './url_drilldown';
import {
SELECT_RANGE_TRIGGER,
RowClickContext,
VALUE_CLICK_TRIGGER,
ROW_CLICK_TRIGGER,
} from '../../../../../../src/plugins/ui_actions/public';
type ContextScopeInput = ActionContext | ActionFactoryContext;
/**
* Part of context scope extracted from an embeddable
* Expose on the scope as: `{{context.panel.id}}`, `{{context.panel.filters.[0]}}`
*/
interface EmbeddableUrlDrilldownContextScope {
/**
* ID of the embeddable panel.
*/
id: string;
/**
* Title of the embeddable panel.
*/
title?: string;
/**
* In case panel supports only 1 index pattern.
*/
indexPatternId?: string;
/**
* In case panel supports more then 1 index pattern.
*/
indexPatternIds?: string[];
query?: Query;
filters?: Filter[];
timeRange?: TimeRange;
savedObjectId?: string;
/**
* In case panel supports only 1 index patterns
*/
indexPatternId?: string;
/**
* In case panel supports more then 1 index patterns
*/
indexPatternIds?: string[];
}
/**
* Url drilldown context scope
* `{{context.$}}`
*/
interface UrlDrilldownContextScope {
panel?: EmbeddableUrlDrilldownContextScope;
}
export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilldownContextScope {
export function getPanelVariables(contextScopeInput: {
embeddable?: IEmbeddable;
}): EmbeddableUrlDrilldownContextScope {
function hasEmbeddable(val: unknown): val is { embeddable: IEmbeddable } {
if (val && typeof val === 'object' && 'embeddable' in val) return true;
return false;
@ -64,41 +70,52 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld
throw new Error(
"UrlDrilldown [getContextScope] can't build scope because embeddable object is missing in context"
);
const embeddable = contextScopeInput.embeddable;
return getEmbeddableVariables(embeddable);
}
function hasSavedObjectId(obj: Record<string, any>): obj is { savedObjectId: string } {
return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string';
}
/**
* @todo Same functionality is implemented in x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts,
* combine both implementations into a common approach.
*/
function getIndexPatternIds(output: EmbeddableOutput): string[] {
function hasIndexPatterns(
_output: Record<string, any>
): _output is { indexPatterns: Array<{ id?: string }> } {
return (
'indexPatterns' in _output &&
Array.isArray(_output.indexPatterns) &&
_output.indexPatterns.length > 0
);
}
return hasIndexPatterns(output)
? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[])
: [];
}
export function getEmbeddableVariables(
embeddable: IEmbeddable
): EmbeddableUrlDrilldownContextScope {
const input = embeddable.getInput();
const output = embeddable.getOutput();
function hasSavedObjectId(obj: Record<string, any>): obj is { savedObjectId: string } {
return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string';
}
function getIndexPatternIds(): string[] {
function hasIndexPatterns(
_output: Record<string, any>
): _output is { indexPatterns: Array<{ id?: string }> } {
return (
'indexPatterns' in _output &&
Array.isArray(_output.indexPatterns) &&
_output.indexPatterns.length > 0
);
}
return hasIndexPatterns(output)
? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[])
: [];
}
const indexPatternsIds = getIndexPatternIds();
return {
panel: cleanEmptyKeys({
id: input.id,
title: output.title ?? input.title,
savedObjectId:
output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined),
query: input.query,
timeRange: input.timeRange,
filters: input.filters,
indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined,
indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined,
}),
};
const indexPatternsIds = getIndexPatternIds(output);
return deleteUndefinedKeys({
id: input.id,
title: output.title ?? input.title,
savedObjectId:
output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined),
query: input.query,
timeRange: input.timeRange,
filters: input.filters,
indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined,
indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined,
});
}
/**
@ -108,7 +125,9 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld
export type UrlDrilldownEventScope =
| ValueClickTriggerEventScope
| RangeSelectTriggerEventScope
| RowClickTriggerEventScope
| ContextMenuTriggerEventScope;
export type EventScopeInput = ActionContext;
export interface ValueClickTriggerEventScope {
key?: string;
@ -122,6 +141,12 @@ export interface RangeSelectTriggerEventScope {
to?: string | number;
}
export interface RowClickTriggerEventScope {
rowIndex: number;
values: Primitive[];
keys: string[];
columnNames: string[];
}
export type ContextMenuTriggerEventScope = object;
export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope {
@ -129,6 +154,8 @@ export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEve
return getEventScopeFromRangeSelectTriggerContext(eventScopeInput);
} else if (isValueClickTriggerContext(eventScopeInput)) {
return getEventScopeFromValueClickTriggerContext(eventScopeInput);
} else if (isRowClickTriggerContext(eventScopeInput)) {
return getEventScopeFromRowClickTriggerContext(eventScopeInput);
} else if (isContextMenuTriggerContext(eventScopeInput)) {
return {};
} else {
@ -141,7 +168,7 @@ function getEventScopeFromRangeSelectTriggerContext(
): RangeSelectTriggerEventScope {
const { table, column: columnIndex, range } = eventScopeInput.data;
const column = table.columns[columnIndex];
return cleanEmptyKeys({
return deleteUndefinedKeys({
key: toPrimitiveOrUndefined(column?.meta.field) as string,
from: toPrimitiveOrUndefined(range[0]) as string | number | undefined,
to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined,
@ -160,7 +187,7 @@ function getEventScopeFromValueClickTriggerContext(
};
});
return cleanEmptyKeys({
return deleteUndefinedKeys({
key: points[0]?.key,
value: points[0]?.value,
negate,
@ -168,37 +195,53 @@ function getEventScopeFromValueClickTriggerContext(
});
}
/**
* @remarks
* Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel)
* `event` variables are mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL
*/
export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventScope {
if (trigger === SELECT_RANGE_TRIGGER) {
return {
key: 'event.key',
from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago
to: new Date().toISOString(),
};
function getEventScopeFromRowClickTriggerContext({
embeddable,
data,
}: RowClickContext): RowClickTriggerEventScope {
const { rowIndex } = data;
const columns = data.columns || data.table.columns.map(({ id }) => id);
const values: Primitive[] = [];
const keys: string[] = [];
const columnNames: string[] = [];
const row = data.table.rows[rowIndex];
for (const columnId of columns) {
const column = data.table.columns.find(({ id }) => id === columnId);
if (!column) {
// This should never happe, but in case it does we log data necessary for debugging.
// eslint-disable-next-line no-console
console.error(data, embeddable ? `Embeddable [${embeddable.getTitle()}]` : null);
throw new Error('Could not find a datatable column.');
}
values.push(row[columnId]);
keys.push(column.meta.field || '');
columnNames.push(column.name || column.meta.field || '');
}
if (trigger === VALUE_CLICK_TRIGGER) {
// number of mock points to generate
// should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER
const nPoints = 4;
const points = new Array(nPoints).fill(0).map((_, index) => ({
key: `event.points.${index}.key`,
value: `event.points.${index}.value`,
}));
return {
key: `event.key`,
value: `event.value`,
negate: false,
points,
};
const scope: RowClickTriggerEventScope = {
rowIndex,
values,
keys,
columnNames,
};
return scope;
}
export function getEventVariableList(context: ActionFactoryContext): string[] {
const [trigger] = context.triggers;
switch (trigger) {
case SELECT_RANGE_TRIGGER:
return ['event.key', 'event.from', 'event.to'];
case VALUE_CLICK_TRIGGER:
return ['event.key', 'event.value', 'event.negate', 'event.points'];
case ROW_CLICK_TRIGGER:
return ['event.rowIndex', 'event.values', 'event.keys', 'event.columnNames'];
}
return {};
return [];
}
type Primitive = string | number | boolean | null;
@ -210,7 +253,7 @@ function toPrimitiveOrUndefined(v: unknown): Primitive | undefined {
return String(v);
}
function cleanEmptyKeys<T extends Record<string, any>>(obj: T): T {
function deleteUndefinedKeys<T extends Record<string, any>>(obj: T): T {
Object.keys(obj).forEach((key) => {
if (obj[key] === undefined) {
delete obj[key];

View file

@ -1,5 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`datatable_expression DatatableComponent it renders actions column when there are row actions 1`] = `
<VisualizationContainer
reportTitle="My fanci metric chart"
>
<EuiBasicTable
className="lnsDataTable"
columns={
Array [
Object {
"field": "a",
"name": "a",
"render": [Function],
},
Object {
"field": "b",
"name": "b",
"render": [Function],
},
Object {
"field": "c",
"name": "c",
"render": [Function],
},
Object {
"actions": Array [
Object {
"description": "Table row context menu",
"icon": [Function],
"name": "More",
"onClick": [Function],
"type": "icon",
},
],
"name": "Actions",
},
]
}
data-test-subj="lnsDataTable"
items={
Array [
Object {
"a": "shoes",
"b": 1588024800000,
"c": 3,
"rowIndex": 0,
},
]
}
noItemsMessage="No items found"
responsive={true}
tableLayout="auto"
/>
</VisualizationContainer>
`;
exports[`datatable_expression DatatableComponent it renders the title and value 1`] = `
<VisualizationContainer
reportTitle="My fanci metric chart"
@ -32,6 +87,7 @@ exports[`datatable_expression DatatableComponent it renders the title and value
"a": "shoes",
"b": 1588024800000,
"c": 3,
"rowIndex": 0,
},
]
}

View file

@ -110,6 +110,24 @@ describe('datatable_expression', () => {
).toMatchSnapshot();
});
test('it renders actions column when there are row actions', () => {
const { data, args } = sampleArgs();
expect(
shallow(
<DatatableComponent
data={data}
args={args}
formatFactory={(x) => x as IFieldFormat}
onClickValue={onClickValue}
getType={jest.fn()}
onRowContextMenuClick={() => undefined}
rowHasRowClickTriggerActions={[true, true, true]}
/>
)
).toMatchSnapshot();
});
test('it invokes executeTriggerActions with correct context on click on top value', () => {
const { args, data } = sampleArgs();

View file

@ -10,13 +10,22 @@ import React, { useMemo } from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import {
EuiBasicTable,
EuiFlexGroup,
EuiButtonIcon,
EuiFlexItem,
EuiToolTip,
EuiBasicTableColumn,
EuiTableActionsColumnType,
} from '@elastic/eui';
import { IAggType } from 'src/plugins/data/public';
import {
FormatFactory,
ILensInterpreterRenderHandlers,
LensFilterEvent,
LensMultiTable,
LensTableRowContextMenuEvent,
} from '../types';
import {
ExpressionFunctionDefinition,
@ -45,7 +54,14 @@ export interface DatatableProps {
type DatatableRenderProps = DatatableProps & {
formatFactory: FormatFactory;
onClickValue: (data: LensFilterEvent['data']) => void;
onRowContextMenuClick?: (data: LensTableRowContextMenuEvent['data']) => void;
getType: (name: string) => IAggType;
/**
* A boolean for each table row, which is true if the row active
* ROW_CLICK_TRIGGER actions attached to it, otherwise false.
*/
rowHasRowClickTriggerActions?: boolean[];
};
export interface DatatableRender {
@ -143,13 +159,47 @@ export const getDatatableRenderer = (dependencies: {
const onClickValue = (data: LensFilterEvent['data']) => {
handlers.event({ name: 'filter', data });
};
const onRowContextMenuClick = (data: LensTableRowContextMenuEvent['data']) => {
handlers.event({ name: 'tableRowContextMenuClick', data });
};
const { hasCompatibleActions } = handlers;
// An entry for each table row, whether it has any actions attached to
// ROW_CLICK_TRIGGER trigger.
let rowHasRowClickTriggerActions: boolean[] = [];
if (hasCompatibleActions) {
const table = Object.values(config.data.tables)[0];
if (!!table) {
rowHasRowClickTriggerActions = await Promise.all(
table.rows.map(async (row, rowIndex) => {
try {
const hasActions = await hasCompatibleActions({
name: 'tableRowContextMenuClick',
data: {
rowIndex,
table,
columns: config.args.columns.columnIds,
},
});
return hasActions;
} catch {
return false;
}
})
);
}
}
ReactDOM.render(
<I18nProvider>
<DatatableComponent
{...config}
formatFactory={resolvedFormatFactory}
onClickValue={onClickValue}
onRowContextMenuClick={onRowContextMenuClick}
getType={resolvedGetType}
rowHasRowClickTriggerActions={rowHasRowClickTriggerActions}
/>
</I18nProvider>,
domNode,
@ -169,7 +219,7 @@ export function DatatableComponent(props: DatatableRenderProps) {
formatters[column.id] = props.formatFactory(column.meta?.params);
});
const { onClickValue } = props;
const { onClickValue, onRowContextMenuClick } = props;
const handleFilterClick = useMemo(
() => (field: string, value: unknown, colIndex: number, negate: boolean = false) => {
const col = firstTable.columns[colIndex];
@ -214,6 +264,124 @@ export function DatatableComponent(props: DatatableRenderProps) {
return <EmptyPlaceholder icon={LensIconChartDatatable} />;
}
const tableColumns: Array<
EuiBasicTableColumn<{ rowIndex: number; [key: string]: unknown }>
> = props.args.columns.columnIds
.map((field) => {
const col = firstTable.columns.find((c) => c.id === field);
const filterable = bucketColumns.includes(field);
const colIndex = firstTable.columns.findIndex((c) => c.id === field);
return {
field,
name: (col && col.name) || '',
render: (value: unknown) => {
const formattedValue = formatters[field]?.convert(value);
const fieldName = col?.meta?.field;
if (filterable) {
return (
<EuiFlexGroup
className="lnsDataTable__cell"
data-test-subj="lnsDataTableCellValueFilterable"
gutterSize="xs"
>
<EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup
responsive={false}
gutterSize="none"
alignItems="center"
className="lnsDataTable__filter"
>
<EuiToolTip
position="bottom"
content={i18n.translate('xpack.lens.includeValueButtonTooltip', {
defaultMessage: 'Include value',
})}
>
<EuiButtonIcon
iconType="plusInCircle"
color="text"
aria-label={i18n.translate('xpack.lens.includeValueButtonAriaLabel', {
defaultMessage: `Include {value}`,
values: {
value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`,
},
})}
data-test-subj="lensDatatableFilterFor"
onClick={() => handleFilterClick(field, value, colIndex)}
/>
</EuiToolTip>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={i18n.translate('xpack.lens.excludeValueButtonTooltip', {
defaultMessage: 'Exclude value',
})}
>
<EuiButtonIcon
iconType="minusInCircle"
color="text"
aria-label={i18n.translate('xpack.lens.excludeValueButtonAriaLabel', {
defaultMessage: `Exclude {value}`,
values: {
value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`,
},
})}
data-test-subj="lensDatatableFilterOut"
onClick={() => handleFilterClick(field, value, colIndex, true)}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return <span data-test-subj="lnsDataTableCellValue">{formattedValue}</span>;
},
};
})
.filter(({ field }) => !!field);
if (!!props.rowHasRowClickTriggerActions && !!onRowContextMenuClick) {
const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions.find((x) => x);
if (hasAtLeastOneRowClickAction) {
const actions: EuiTableActionsColumnType<{ rowIndex: number; [key: string]: unknown }> = {
name: i18n.translate('xpack.lens.datatable.actionsColumnName', {
defaultMessage: 'Actions',
}),
actions: [
{
name: i18n.translate('xpack.lens.tableRowMore', {
defaultMessage: 'More',
}),
description: i18n.translate('xpack.lens.tableRowMoreDescription', {
defaultMessage: 'Table row context menu',
}),
type: 'icon',
icon: ({ rowIndex }: { rowIndex: number }) => {
if (
!!props.rowHasRowClickTriggerActions &&
!props.rowHasRowClickTriggerActions[rowIndex]
)
return 'empty';
return 'boxesVertical';
},
onClick: ({ rowIndex }) => {
onRowContextMenuClick({
rowIndex,
table: firstTable,
columns: props.args.columns.columnIds,
});
},
},
],
};
tableColumns.push(actions);
}
}
return (
<VisualizationContainer
reportTitle={props.args.title}
@ -223,89 +391,8 @@ export function DatatableComponent(props: DatatableRenderProps) {
className="lnsDataTable"
data-test-subj="lnsDataTable"
tableLayout="auto"
columns={props.args.columns.columnIds
.map((field) => {
const col = firstTable.columns.find((c) => c.id === field);
const filterable = bucketColumns.includes(field);
const colIndex = firstTable.columns.findIndex((c) => c.id === field);
return {
field,
name: (col && col.name) || '',
render: (value: unknown) => {
const formattedValue = formatters[field]?.convert(value);
const fieldName = col?.meta?.field;
if (filterable) {
return (
<EuiFlexGroup
className="lnsDataTable__cell"
data-test-subj="lnsDataTableCellValueFilterable"
gutterSize="xs"
>
<EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup
responsive={false}
gutterSize="none"
alignItems="center"
className="lnsDataTable__filter"
>
<EuiToolTip
position="bottom"
content={i18n.translate('xpack.lens.includeValueButtonTooltip', {
defaultMessage: 'Include value',
})}
>
<EuiButtonIcon
iconType="plusInCircle"
color="text"
aria-label={i18n.translate('xpack.lens.includeValueButtonAriaLabel', {
defaultMessage: `Include {value}`,
values: {
value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`,
},
})}
data-test-subj="lensDatatableFilterFor"
onClick={() => handleFilterClick(field, value, colIndex)}
/>
</EuiToolTip>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={i18n.translate('xpack.lens.excludeValueButtonTooltip', {
defaultMessage: 'Exclude value',
})}
>
<EuiButtonIcon
iconType="minusInCircle"
color="text"
aria-label={i18n.translate(
'xpack.lens.excludeValueButtonAriaLabel',
{
defaultMessage: `Exclude {value}`,
values: {
value: `${
fieldName ? `${fieldName}: ` : ''
}${formattedValue}`,
},
}
)}
data-test-subj="lensDatatableFilterOut"
onClick={() => handleFilterClick(field, value, colIndex, true)}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return <span data-test-subj="lnsDataTableCellValue">{formattedValue}</span>;
},
};
})
.filter(({ field }) => !!field)}
items={firstTable ? firstTable.rows : []}
columns={tableColumns}
items={firstTable ? firstTable.rows.map((row, rowIndex) => ({ ...row, rowIndex })) : []}
/>
</VisualizationContainer>
);

View file

@ -7,11 +7,9 @@
import { CoreSetup } from 'kibana/public';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
import { EditorFrameSetup, FormatFactory } from '../types';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
interface DatatableVisualizationPluginStartPlugins {
uiActions: UiActionsStart;
data: DataPublicPluginStart;
}
export interface DatatableVisualizationPluginSetupPlugins {
@ -34,6 +32,7 @@ export class DatatableVisualization {
getDatatableRenderer,
datatableVisualization,
} = await import('../async_services');
expressions.registerFunction(() => datatableColumns);
expressions.registerFunction(() => datatable);
expressions.registerRenderer(() =>

View file

@ -25,7 +25,7 @@ import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable';
import { coreMock, httpServiceMock } from '../../../../../../src/core/public/mocks';
import { IBasePath } from '../../../../../../src/core/public';
import { AttributeService } from '../../../../../../src/plugins/embeddable/public';
import { AttributeService, ViewMode } from '../../../../../../src/plugins/embeddable/public';
import { LensAttributeService } from '../../lens_attribute_service';
import { OnSaveProps } from '../../../../../../src/plugins/saved_objects/public/save_modal';
import { act } from 'react-dom/test-utils';
@ -221,6 +221,74 @@ describe('embeddable', () => {
expect(expressionRenderer).toHaveBeenCalledTimes(2);
});
it('should re-render when dashboard view/edit mode changes', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
expect(expressionRenderer).toHaveBeenCalledTimes(1);
embeddable.updateInput({
viewMode: ViewMode.VIEW,
});
expect(expressionRenderer).toHaveBeenCalledTimes(2);
});
it('should re-render when dynamic actions input changes', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
expect(expressionRenderer).toHaveBeenCalledTimes(1);
embeddable.updateInput({
enhancements: {
dynamicActions: {},
},
});
expect(expressionRenderer).toHaveBeenCalledTimes(2);
});
it('should pass context to embeddable', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
@ -396,6 +464,37 @@ describe('embeddable', () => {
);
});
it('should execute trigger on row click event from expression renderer', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
const onEvent = expressionRenderer.mock.calls[0][0].onEvent!;
onEvent({ name: 'tableRowContextMenuClick', data: {} });
expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick);
});
it('should not re-render if only change is in disabled filter', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };

View file

@ -21,6 +21,8 @@ import { PaletteOutput } from 'src/plugins/charts/public';
import { Subscription } from 'rxjs';
import { toExpression, Ast } from '@kbn/interpreter/common';
import { RenderMode } from 'src/plugins/expressions';
import { map, distinctUntilChanged, skip } from 'rxjs/operators';
import isEqual from 'fast-deep-equal';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
@ -38,7 +40,11 @@ import {
import { Document, injectFilterReferences } from '../../persistence';
import { ExpressionWrapper } from './expression_wrapper';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { isLensBrushEvent, isLensFilterEvent } from '../../types';
import {
isLensBrushEvent,
isLensFilterEvent,
isLensTableRowContextMenuClickEvent,
} from '../../types';
import { IndexPatternsContract } from '../../../../../../src/plugins/data/public';
import { getEditPath, DOC_TYPE } from '../../../common';
@ -71,6 +77,7 @@ export interface LensEmbeddableDeps {
timefilter: TimefilterContract;
basePath: IBasePath;
getTrigger?: UiActionsStart['getTrigger'] | undefined;
getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions'];
}
export class Embeddable
@ -117,6 +124,36 @@ export class Embeddable
this.autoRefreshFetchSubscription = deps.timefilter
.getAutoRefreshFetch$()
.subscribe(this.reload.bind(this));
const input$ = this.getInput$();
// Lens embeddable does not re-render when embeddable input changes in
// general, to improve performance. This line makes sure the Lens embeddable
// re-renders when anything in ".dynamicActions" (e.g. drilldowns) changes.
input$
.pipe(
map((input) => input.enhancements?.dynamicActions),
distinctUntilChanged((a, b) => isEqual(a, b)),
skip(1)
)
.subscribe((input) => {
this.reload();
});
// Lens embeddable does not re-render when embeddable input changes in
// general, to improve performance. This line makes sure the Lens embeddable
// re-renders when dashboard view mode switches between "view/edit". This is
// needed to see the changes to ".dynamicActions" (e.g. drilldowns) when
// dashboard's mode is toggled.
input$
.pipe(
map((input) => input.viewMode),
distinctUntilChanged(),
skip(1)
)
.subscribe((input) => {
this.reload();
});
}
public supportedTriggers() {
@ -127,6 +164,7 @@ export class Embeddable
case 'lnsXY':
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
case 'lnsDatatable':
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick];
case 'lnsPie':
return [VIS_EVENT_TO_TRIGGER.filter];
case 'lnsMetric':
@ -217,11 +255,31 @@ export class Embeddable
handleEvent={this.handleEvent}
onData$={this.updateActiveData}
renderMode={input.renderMode}
hasCompatibleActions={this.hasCompatibleActions}
/>,
domNode
);
}
private readonly hasCompatibleActions = async (
event: ExpressionRendererEvent
): Promise<boolean> => {
if (isLensTableRowContextMenuClickEvent(event)) {
const { getTriggerCompatibleActions } = this.deps;
if (!getTriggerCompatibleActions) {
return false;
}
const actions = await getTriggerCompatibleActions(VIS_EVENT_TO_TRIGGER[event.name], {
data: event.data,
embeddable: this,
});
return actions.length > 0;
}
return false;
};
/**
* Combines the embeddable context with the saved object context, and replaces
* any references to index patterns
@ -264,6 +322,16 @@ export class Embeddable
embeddable: this,
});
}
if (isLensTableRowContextMenuClickEvent(event)) {
this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec(
{
data: event.data,
embeddable: this,
},
true
);
}
};
async reload() {

View file

@ -94,6 +94,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
editable: await this.isEditable(),
basePath: coreHttp.basePath,
getTrigger: uiActions?.getTrigger,
getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions,
documentToExpression,
},
input,

View file

@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
ReactExpressionRendererProps,
} from 'src/plugins/expressions/public';
import { ExecutionContextSearch } from 'src/plugins/data/public';
import { RenderMode } from 'src/plugins/expressions';
@ -26,6 +27,7 @@ export interface ExpressionWrapperProps {
handleEvent: (event: ExpressionRendererEvent) => void;
onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void;
renderMode?: RenderMode;
hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions'];
}
export function ExpressionWrapper({
@ -37,6 +39,7 @@ export function ExpressionWrapper({
searchSessionId,
onData$,
renderMode,
hasCompatibleActions,
}: ExpressionWrapperProps) {
return (
<I18nProvider>
@ -80,6 +83,7 @@ export function ExpressionWrapper({
</div>
)}
onEvent={handleEvent}
hasCompatibleActions={hasCompatibleActions}
/>
</div>
)}

View file

@ -8,6 +8,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
import { CoreSetup } from 'kibana/public';
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
import { SavedObjectReference } from 'kibana/public';
import { ROW_CLICK_TRIGGER } from '../../../../src/plugins/ui_actions/public';
import {
ExpressionAstExpression,
ExpressionRendererEvent,
@ -614,11 +615,17 @@ export interface LensFilterEvent {
name: 'filter';
data: TriggerContext<typeof VALUE_CLICK_TRIGGER>['data'];
}
export interface LensBrushEvent {
name: 'brush';
data: TriggerContext<typeof SELECT_RANGE_TRIGGER>['data'];
}
export interface LensTableRowContextMenuEvent {
name: 'tableRowContextMenuClick';
data: TriggerContext<typeof ROW_CLICK_TRIGGER>['data'];
}
export function isLensFilterEvent(event: ExpressionRendererEvent): event is LensFilterEvent {
return event.name === 'filter';
}
@ -627,11 +634,17 @@ export function isLensBrushEvent(event: ExpressionRendererEvent): event is LensB
return event.name === 'brush';
}
export function isLensTableRowContextMenuClickEvent(
event: ExpressionRendererEvent
): event is LensBrushEvent {
return event.name === 'tableRowContextMenuClick';
}
/**
* Expression renderer handlers specifically for lens renderers. This is a narrowed down
* version of the general render handlers, specifying supported event types. If this type is
* used, dispatched events will be handled correctly.
*/
export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandlers {
event: (event: LensFilterEvent | LensBrushEvent) => void;
event: (event: LensFilterEvent | LensBrushEvent | LensTableRowContextMenuEvent) => void;
}

View file

@ -5,7 +5,7 @@
*/
import React from 'react';
import { UrlDrilldownConfig, UrlDrilldownScope } from '../../../types';
import { UrlDrilldownConfig } from '../../../types';
import { UrlDrilldownCollectConfig } from '../url_drilldown_collect_config';
export const Demo = () => {
@ -14,33 +14,13 @@ export const Demo = () => {
url: { template: '' },
});
const fakeScope: UrlDrilldownScope = {
kibanaUrl: 'http://localhost:5601/',
context: {
filters: [
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
},
{
query: { match: { _type: { query: 'nginx', type: 'phrase' } } },
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
},
],
},
event: {
key: 'fakeKey',
value: 'fakeValue',
},
};
return (
<>
<UrlDrilldownCollectConfig config={config} onConfig={onConfig} scope={fakeScope} />
<UrlDrilldownCollectConfig
config={config}
onConfig={onConfig}
variables={['event.key', 'event.value']}
/>
{JSON.stringify(config)}
</>
);

View file

@ -1,47 +0,0 @@
/*
* 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 { Demo } from './test_samples/demo';
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
test('configure valid URL template', () => {
const screen = render(<Demo />);
const urlTemplate = 'https://elastic.co/?{{event.key}}={{event.value}}';
fireEvent.change(screen.getByLabelText(/Enter URL template/i), {
target: { value: urlTemplate },
});
const preview = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement;
expect(preview.value).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`);
expect(preview.disabled).toEqual(true);
const previewLink = screen.getByText('Preview') as HTMLAnchorElement;
expect(previewLink.href).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`);
expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`);
});
test('configure invalid URL template', () => {
const screen = render(<Demo />);
const urlTemplate = 'https://elastic.co/?{{event.wrongKey}}={{event.wrongValue}}';
fireEvent.change(screen.getByLabelText(/Enter URL template/i), {
target: { value: urlTemplate },
});
const previewTextArea = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement;
expect(previewTextArea.disabled).toEqual(true);
expect(previewTextArea.value).toEqual(urlTemplate);
expect(screen.getByText(/invalid format/i)).toBeInTheDocument(); // check that error is shown
const previewLink = screen.getByText('Preview') as HTMLAnchorElement;
expect(previewLink.href).toEqual(urlTemplate);
expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`);
});

View file

@ -18,52 +18,40 @@ import {
EuiTextArea,
EuiSelectableOption,
} from '@elastic/eui';
import { UrlDrilldownConfig, UrlDrilldownScope } from '../../types';
import { compile } from '../../url_template';
import { validateUrlTemplate } from '../../url_validation';
import { buildScopeSuggestions } from '../../url_drilldown_scope';
import { UrlDrilldownConfig } from '../../types';
import './index.scss';
import {
txtAddVariableButtonTitle,
txtUrlPreviewHelpText,
txtUrlTemplateSyntaxHelpLinkText,
txtUrlTemplateVariablesHelpLinkText,
txtUrlTemplateVariablesFilterPlaceholderText,
txtUrlTemplateLabel,
txtUrlTemplateOpenInNewTab,
txtUrlTemplatePlaceholder,
txtUrlTemplatePreviewLabel,
txtUrlTemplatePreviewLinkText,
} from './i18n';
export interface UrlDrilldownCollectConfig {
config: UrlDrilldownConfig;
variables: string[];
onConfig: (newConfig: UrlDrilldownConfig) => void;
scope: UrlDrilldownScope;
syntaxHelpDocsLink?: string;
variablesHelpDocsLink?: string;
}
export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
config,
variables,
onConfig,
scope,
syntaxHelpDocsLink,
variablesHelpDocsLink,
}) => {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [showUrlError, setShowUrlError] = React.useState(false);
const urlTemplate = config.url.template ?? '';
const compiledUrl = React.useMemo(() => {
try {
return compile(urlTemplate, scope);
} catch {
return urlTemplate;
}
}, [urlTemplate, scope]);
const scopeVariables = React.useMemo(() => buildScopeSuggestions(scope), [scope]);
function updateUrlTemplate(newUrlTemplate: string) {
if (config.url.template !== newUrlTemplate) {
setShowUrlError(true);
onConfig({
...config,
url: {
@ -73,18 +61,31 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
});
}
}
const { error, isValid } = React.useMemo(
() => validateUrlTemplate({ template: urlTemplate }, scope),
[urlTemplate, scope]
);
const isEmpty = !urlTemplate;
const isInvalid = !isValid && !isEmpty;
const isInvalid = showUrlError && isEmpty;
const variablesDropdown = (
<AddVariableButton
variables={variables}
variablesHelpLink={variablesHelpDocsLink}
onSelect={(variable: string) => {
if (textAreaRef.current) {
updateUrlTemplate(
urlTemplate.substr(0, textAreaRef.current!.selectionStart) +
`{{${variable}}}` +
urlTemplate.substr(textAreaRef.current!.selectionEnd)
);
} else {
updateUrlTemplate(urlTemplate + `{{${variable}}}`);
}
}}
/>
);
return (
<>
<EuiFormRow
fullWidth
isInvalid={isInvalid}
error={error}
className={'uaeUrlDrilldownCollectConfig__urlTemplateFormRow'}
label={txtUrlTemplateLabel}
helpText={
@ -94,23 +95,7 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
</EuiLink>
)
}
labelAppend={
<AddVariableButton
variables={scopeVariables}
variablesHelpLink={variablesHelpDocsLink}
onSelect={(variable: string) => {
if (textAreaRef.current) {
updateUrlTemplate(
urlTemplate.substr(0, textAreaRef.current!.selectionStart) +
`{{${variable}}}` +
urlTemplate.substr(textAreaRef.current!.selectionEnd)
);
} else {
updateUrlTemplate(urlTemplate + `{{${variable}}}`);
}
}}
/>
}
labelAppend={variablesDropdown}
>
<EuiTextArea
fullWidth
@ -120,31 +105,11 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
value={urlTemplate}
placeholder={txtUrlTemplatePlaceholder}
onChange={(event) => updateUrlTemplate(event.target.value)}
onBlur={() => setShowUrlError(true)}
rows={3}
inputRef={textAreaRef}
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={txtUrlTemplatePreviewLabel}
labelAppend={
<EuiText size="xs">
<EuiLink href={compiledUrl} target="_blank" external>
{txtUrlTemplatePreviewLinkText}
</EuiLink>
</EuiText>
}
helpText={txtUrlPreviewHelpText}
>
<EuiTextArea
fullWidth
name="urlPreview"
data-test-subj="urlPreview"
value={compiledUrl}
disabled={true}
rows={3}
/>
</EuiFormRow>
<EuiFormRow hasChildLabel={false}>
<EuiCheckbox
id="openInNewTab"

View file

@ -12,7 +12,3 @@ export {
} from './url_validation';
export { compile as urlDrilldownCompileUrl } from './url_template';
export { globalScopeProvider as urlDrilldownGlobalScopeProvider } from './url_drilldown_global_scope';
export {
buildScope as urlDrilldownBuildScope,
buildScopeSuggestions as urlDrilldownBuildScopeSuggestions,
} from './url_drilldown_scope';

View file

@ -1,52 +0,0 @@
/*
* 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 { buildScope, buildScopeSuggestions } from './url_drilldown_scope';
test('buildScopeSuggestions', () => {
expect(
buildScopeSuggestions(
buildScope({
globalScope: {
kibanaUrl: 'http://localhost:5061/',
},
eventScope: {
key: '__testKey__',
value: '__testValue__',
},
contextScope: {
filters: [
{
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
},
{
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
},
{
query: { match: { _type: { query: 'nginx', type: 'phrase' } } },
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
},
],
query: {
query: '',
language: 'kquery',
},
},
})
)
).toMatchInlineSnapshot(`
Array [
"event.key",
"event.value",
"context.filters",
"context.query.language",
"context.query.query",
"kibanaUrl",
]
`);
});

View file

@ -1,39 +0,0 @@
/*
* 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 { partition } from 'lodash';
import { getFlattenedObject } from '@kbn/std';
import { UrlDrilldownGlobalScope, UrlDrilldownScope } from './types';
export function buildScope<
ContextScope extends object = object,
EventScope extends object = object
>({
globalScope,
contextScope,
eventScope,
}: {
globalScope: UrlDrilldownGlobalScope;
contextScope?: ContextScope;
eventScope?: EventScope;
}): UrlDrilldownScope<ContextScope, EventScope> {
return {
...globalScope,
context: contextScope,
event: eventScope,
};
}
/**
* Builds list of variables for suggestion from scope
* keys sorted alphabetically, except {{event.$}} variables are pulled to the top
* @param scope
*/
export function buildScopeSuggestions(scope: UrlDrilldownGlobalScope): string[] {
const allKeys = Object.keys(getFlattenedObject(scope)).sort();
const [eventKeys, otherKeys] = partition(allKeys, (key) => key.startsWith('event'));
return [...eventKeys, ...otherKeys];
}