diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md
index 1c9fc27d53f19..9447c8a4e50a7 100644
--- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-export declare type ChartActionContext = ValueClickContext | RangeSelectContext;
+export declare type ChartActionContext = ValueClickContext | RangeSelectContext | RowClickContext;
```
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isembeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isembeddable.md
new file mode 100644
index 0000000000000..ea8d3870dc055
--- /dev/null
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isembeddable.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isEmbeddable](./kibana-plugin-plugins-embeddable-public.isembeddable.md)
+
+## isEmbeddable variable
+
+Signature:
+
+```typescript
+isEmbeddable: (x: unknown) => x is IEmbeddable
+```
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md
new file mode 100644
index 0000000000000..91e0f988db69c
--- /dev/null
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isRowClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md)
+
+## isRowClickTriggerContext variable
+
+Signature:
+
+```typescript
+isRowClickTriggerContext: (context: ChartActionContext) => context is RowClickContext
+```
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md
index 06f792837e4fe..f1ea605703e59 100644
--- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md
@@ -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 input
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) | |
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md
index fcccd3f6b9618..1565202e84674 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md
@@ -9,7 +9,7 @@ Constructs a new instance of the `ExpressionRenderHandler` class
Signature:
```typescript
-constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial);
+constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
```
## Parameters
@@ -17,5 +17,5 @@ constructor(element: HTMLElement, { onRenderError, renderMode }?: PartialHTMLElement | |
-| { onRenderError, renderMode } | Partial<ExpressionRenderHandlerParams>
| |
+| { onRenderError, renderMode, hasCompatibleActions, } | ExpressionRenderHandlerParams
| |
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md
index 12c663273bd8c..d65c06bdaed83 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md
@@ -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 ExpressionRenderHandler
class |
+| [(constructor)(element, { onRenderError, renderMode, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the ExpressionRenderHandler
class |
## Properties
@@ -24,7 +24,7 @@ export declare class ExpressionRenderHandler
| [events$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.events_.md) | | Observable<ExpressionRendererEvent>
| |
| [getElement](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.getelement.md) | | () => HTMLElement
| |
| [handleRenderError](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.handlerendererror.md) | | (error: ExpressionRenderError) => void
| |
-| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | (data: any, uiState?: any) => Promise<void>
| |
+| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | (value: any, uiState?: any) => Promise<void>
| |
| [render$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render_.md) | | Observable<number>
| |
| [update$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.update_.md) | | Observable<UpdateValue | null>
| |
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md
index dec17d60ffd14..87f378fd58344 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-render: (data: any, uiState?: any) => Promise;
+render: (value: any, uiState?: any) => Promise;
```
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md
new file mode 100644
index 0000000000000..4d2b76cb323fb
--- /dev/null
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md)
+
+## IExpressionLoaderParams.hasCompatibleActions property
+
+Signature:
+
+```typescript
+hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
+```
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md
index 54eecad0deb50..22a73fff039e6 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md
@@ -19,6 +19,7 @@ export interface IExpressionLoaderParams
| [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | []
| |
| [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | boolean
| |
| [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean
| |
+| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | ExpressionRenderHandlerParams['hasCompatibleActions']
| |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters
| |
| [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType
| |
| [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode
| |
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md
new file mode 100644
index 0000000000000..d178af55ae2d9
--- /dev/null
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) > [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md)
+
+## IInterpreterRenderHandlers.hasCompatibleActions property
+
+Signature:
+
+```typescript
+hasCompatibleActions?: (event: any) => Promise;
+```
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md
index a65e025451636..931e474a41006 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md
@@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers
| [done](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.done.md) | () => void
| Done increments the number of rendering successes |
| [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | (event: any) => void
| |
| [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode
| |
+| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean>
| |
| [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void
| |
| [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | () => void
| |
| [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | PersistedState
| |
diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md
new file mode 100644
index 0000000000000..55419279f5d21
--- /dev/null
+++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) > [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md)
+
+## IInterpreterRenderHandlers.hasCompatibleActions property
+
+Signature:
+
+```typescript
+hasCompatibleActions?: (event: any) => Promise;
+```
diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md
index b1496386944fa..273703cacca06 100644
--- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md
+++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md
@@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers
| [done](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.done.md) | () => void
| Done increments the number of rendering successes |
| [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | (event: any) => void
| |
| [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode
| |
+| [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean>
| |
| [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void
| |
| [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | () => void
| |
| [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | PersistedState
| |
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md
index 5e10de4e0f2a5..fd1ea7df4fb74 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md
@@ -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\*.Trigger
specifies a named anchor to which Action
can be attached. When Trigger
is being \*called\* it creates a Context
object and passes it to the execute
method of an Action
.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) | |
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.row_click_trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.row_click_trigger.md
new file mode 100644
index 0000000000000..3541b53ab1d61
--- /dev/null
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.row_click_trigger.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.row_click_trigger.md)
+
+## ROW\_CLICK\_TRIGGER variable
+
+Signature:
+
+```typescript
+ROW_CLICK_TRIGGER = "ROW_CLICK_TRIGGER"
+```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md
new file mode 100644
index 0000000000000..1068cc9146893
--- /dev/null
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) > [data](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md)
+
+## RowClickContext.data property
+
+Signature:
+
+```typescript
+data: {
+ rowIndex: number;
+ table: Datatable;
+ columns?: string[];
+ };
+```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md
new file mode 100644
index 0000000000000..e8baf44ff9cbc
--- /dev/null
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) > [embeddable](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md)
+
+## RowClickContext.embeddable property
+
+Signature:
+
+```typescript
+embeddable?: IEmbeddable;
+```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.md
new file mode 100644
index 0000000000000..74b55d85f10e3
--- /dev/null
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md)
+
+## RowClickContext interface
+
+Signature:
+
+```typescript
+export interface RowClickContext
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [data](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md) | {
rowIndex: number;
table: Datatable;
columns?: string[];
}
| |
+| [embeddable](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md) | IEmbeddable
| |
+
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md
new file mode 100644
index 0000000000000..aa1097d8c0864
--- /dev/null
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [rowClickTrigger](./kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md)
+
+## rowClickTrigger variable
+
+Signature:
+
+```typescript
+rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'>
+```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md
index 9db44d4dc7b05..2f0d22cf6dd74 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md
@@ -16,6 +16,7 @@ export interface TriggerContextMapping
| --- | --- | --- |
| [""](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md) | TriggerContext
| |
| [FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.filter_trigger.md) | ApplyGlobalFilterActionContext
| |
+| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md) | RowClickContext
| |
| [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.select_range_trigger.md) | RangeSelectContext
| |
| [VALUE\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.value_click_trigger.md) | ValueClickContext
| |
| [VISUALIZE\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md) | VisualizeFieldContext
| |
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md
new file mode 100644
index 0000000000000..cf253df337378
--- /dev/null
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) > [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md)
+
+## TriggerContextMapping.ROW\_CLICK\_TRIGGER property
+
+Signature:
+
+```typescript
+[ROW_CLICK_TRIGGER]: RowClickContext;
+```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md
index fd6ade88479af..ca999322b7a56 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md
@@ -11,5 +11,5 @@
Signature:
```typescript
-readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void;
+readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void;
```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md
index 19f215a96b23b..e95e7e1eb38b6 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-readonly attachAction: (triggerId: T, actionId: string) => void;
+readonly attachAction: (triggerId: T, actionId: string) => void;
```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md
index 1bb6ca1115248..8e7fb8b8bbf29 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md
@@ -12,5 +12,5 @@
Signature:
```typescript
-readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise;
+readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise;
```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md
index d44dc4e43a52e..b996620686a28 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-readonly getTrigger: (triggerId: T) => TriggerContract;
+readonly getTrigger: (triggerId: T) => TriggerContract;
```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md
index 0a9b674a45de2..f94b34ecc2d90 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-readonly getTriggerActions: (triggerId: T) => Action[];
+readonly getTriggerActions: (triggerId: T) => Action[];
```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md
index faed81236342d..dff958608ef9e 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>;
+readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>;
```
diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md
index e3c5dbb92ae90..e35eb503ab62b 100644
--- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md
+++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md
@@ -21,17 +21,17 @@ export declare class UiActionsService
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry
| |
-| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <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
| addTriggerAction
is similar to attachAction
as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction
also infers better typing of the action
argument. |
-| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <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
| |
+| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <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
| addTriggerAction
is similar to attachAction
as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction
also infers better typing of the action
argument. |
+| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <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
| |
| [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void
| Removes all registered triggers and actions. |
| [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: TriggerId, actionId: string) => void
| |
-| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <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>
| |
+| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <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>
| |
| [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | UiActionsExecutionService
| |
| [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | () => UiActionsService
| "Fork" a separate instance of UiActionsService
that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of UiActionsService
are only available within this instance. |
| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<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">
| |
-| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <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>
| |
-| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <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">[]
| |
-| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <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">[]>
| |
+| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <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>
| |
+| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <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">[]
| |
+| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <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">[]>
| |
| [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | (actionId: string) => boolean
| |
| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "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">
| |
| [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | (trigger: Trigger) => void
| |
diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc
index ff2c321f667c8..3db5bd6d97ff0 100644
--- a/docs/user/dashboard/drilldowns.asciidoc
+++ b/docs/user/dashboard/drilldowns.asciidoc
@@ -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 <> 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]
diff --git a/docs/user/dashboard/images/url_drilldown_url_template.png b/docs/user/dashboard/images/url_drilldown_url_template.png
index d8515afe66a80..746ce62733618 100644
Binary files a/docs/user/dashboard/images/url_drilldown_url_template.png and b/docs/user/dashboard/images/url_drilldown_url_template.png differ
diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc
index 617fae938f8f5..df9fa2dca81fd 100644
--- a/docs/user/dashboard/url-drilldown.asciidoc
+++ b/docs/user/dashboard/url-drilldown.asciidoc
@@ -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
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index f5205df1e8024..6a5ee31df6f34 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -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
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index 0d2dcf208f2ef..0fc7c7965010b 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -54,6 +54,7 @@ export {
ErrorEmbeddable,
IContainer,
IEmbeddable,
+ isEmbeddable,
isErrorEmbeddable,
openAddPanelFlyout,
OutputSpec,
@@ -70,6 +71,7 @@ export {
isSavedObjectEmbeddableInput,
isRangeSelectTriggerContext,
isValueClickTriggerContext,
+ isRowClickTriggerContext,
isContextMenuTriggerContext,
EmbeddableStateTransfer,
EmbeddableEditorState,
diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
index 5a73df2e13861..a19ec2345db8d 100644
--- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
@@ -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;
diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts
index 2f6de1be60c9c..4c2baa3bbf809 100644
--- a/src/plugins/embeddable/public/lib/embeddables/index.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/index.ts
@@ -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';
diff --git a/src/plugins/embeddable/public/lib/embeddables/is_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/is_embeddable.ts
new file mode 100644
index 0000000000000..e660fdbc4472c
--- /dev/null
+++ b/src/plugins/embeddable/public/lib/embeddables/is_embeddable.ts
@@ -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;
+};
diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts
index b2965b55dbdfa..c3b1496b8eca8 100644
--- a/src/plugins/embeddable/public/lib/triggers/triggers.ts
+++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts
@@ -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 {
export type ChartActionContext =
| ValueClickContext
- | RangeSelectContext;
+ | RangeSelectContext
+ | 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' &&
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index e42eaaf86bdf3..4b7d60b4dc9ec 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -176,10 +176,11 @@ export class AttributeService>;
}
+// 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 = ValueClickContext | RangeSelectContext;
+export type ChartActionContext = ValueClickContext | RangeSelectContext | 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 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;
+
// 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)
diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts
index dd3124c7d17ee..88aca4c07ee31 100644
--- a/src/plugins/expressions/common/expression_renderers/types.ts
+++ b/src/plugins/expressions/common/expression_renderers/types.ts
@@ -82,6 +82,7 @@ export interface IInterpreterRenderHandlers {
reload: () => void;
update: (params: any) => void;
event: (event: any) => void;
+ hasCompatibleActions?: (event: any) => Promise;
getRenderMode: () => RenderMode;
uiState?: PersistedState;
}
diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts
index 983a344c0e1a1..e9e0fa18af6c3 100644
--- a/src/plugins/expressions/public/loader.ts
+++ b/src/plugins/expressions/public/loader.ts
@@ -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$;
diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md
index 97ff00db0966c..6eb0e71c58e3f 100644
--- a/src/plugins/expressions/public/public.api.md
+++ b/src/plugins/expressions/public/public.api.md
@@ -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);
+ constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
// (undocumented)
destroy: () => void;
// (undocumented)
@@ -544,7 +544,7 @@ export class ExpressionRenderHandler {
// (undocumented)
render$: Observable;
// (undocumented)
- render: (data: any, uiState?: any) => Promise;
+ render: (value: any, uiState?: any) => Promise;
// 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;
+ // (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)
reload: () => void;
diff --git a/src/plugins/expressions/public/render.test.ts b/src/plugins/expressions/public/render.test.ts
index c44683f6779c0..3fc0100db489d 100644
--- a/src/plugins/expressions/public/render.test.ts
+++ b/src/plugins/expressions/public/render.test.ts
@@ -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);
diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts
index 4390033b5be60..717776a2861b4 100644
--- a/src/plugins/expressions/public/render.ts
+++ b/src/plugins/expressions/public/render.ts
@@ -29,8 +29,9 @@ import { getRenderersRegistry } from './services';
export type IExpressionRendererExtraHandlers = Record;
export interface ExpressionRenderHandlerParams {
- onRenderError: RenderErrorHandlerFnType;
- renderMode: RenderMode;
+ onRenderError?: RenderErrorHandlerFnType;
+ renderMode?: RenderMode;
+ hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise;
}
export interface ExpressionRendererEvent {
@@ -59,7 +60,11 @@ export class ExpressionRenderHandler {
constructor(
element: HTMLElement,
- { onRenderError, renderMode }: Partial = {}
+ {
+ 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
+ options?: ExpressionRenderHandlerParams
): ExpressionRenderHandler {
const handler = new ExpressionRenderHandler(element, options);
handler.render(data);
diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts
index 5bae985699476..f37107abbb716 100644
--- a/src/plugins/expressions/public/types/index.ts
+++ b/src/plugins/expressions/public/types/index.ts
@@ -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 {
diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md
index 761ddba8f9270..7c1ab11f75027 100644
--- a/src/plugins/expressions/server/server.api.md
+++ b/src/plugins/expressions/server/server.api.md
@@ -736,6 +736,8 @@ export interface IInterpreterRenderHandlers {
// (undocumented)
getRenderMode: () => RenderMode;
// (undocumented)
+ hasCompatibleActions?: (event: any) => Promise;
+ // (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)
reload: () => void;
diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts
index b9f4a4a0426bf..d223c0abcccb7 100644
--- a/src/plugins/ui_actions/public/index.ts
+++ b/src/plugins/ui_actions/public/index.ts
@@ -50,6 +50,9 @@ export {
visualizeFieldTrigger,
VISUALIZE_GEO_FIELD_TRIGGER,
visualizeGeoFieldTrigger,
+ ROW_CLICK_TRIGGER,
+ rowClickTrigger,
+ RowClickContext,
} from './triggers';
export {
TriggerContextMapping,
diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts
index 87a1df959ccbd..fdb75e9a426e9 100644
--- a/src/plugins/ui_actions/public/plugin.ts
+++ b/src/plugins/ui_actions/public/plugin.ts
@@ -23,6 +23,7 @@ import { UiActionsService } from './service';
import {
selectRangeTrigger,
valueClickTrigger,
+ rowClickTrigger,
applyFilterTrigger,
visualizeFieldTrigger,
visualizeGeoFieldTrigger,
@@ -48,6 +49,7 @@ export class UiActionsPlugin implements Plugin {
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);
diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md
index ca27e19b247c2..2384dfab13c8c 100644
--- a/src/plugins/ui_actions/public/public.api.md
+++ b/src/plugins/ui_actions/public/public.api.md
@@ -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: (triggerId: T, action: UiActionsActionDefinition | Action) => void;
+ readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void;
// (undocumented)
- readonly attachAction: (triggerId: T, actionId: string) => void;
+ readonly attachAction: (triggerId: T, actionId: string) => void;
readonly clear: () => void;
// (undocumented)
readonly detachAction: (triggerId: TriggerId, actionId: string) => void;
// @deprecated (undocumented)
- readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise;
+ readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise;
// 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: (triggerId: T) => TriggerContract;
+ readonly getTrigger: (triggerId: T) => TriggerContract;
// (undocumented)
- readonly getTriggerActions: (triggerId: T) => Action[];
+ readonly getTriggerActions: (triggerId: T) => Action[];
// (undocumented)
- readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>;
+ readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>;
// (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)
```
diff --git a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts
index 4f0ab52501a95..59616dcf3f38d 100644
--- a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts
+++ b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts
@@ -29,6 +29,7 @@ interface ExecuteActionTask {
context: BaseContext;
trigger: Trigger;
defer: Defer;
+ alwaysShowPopup?: boolean;
}
export class UiActionsExecutionService {
@@ -37,21 +38,25 @@ export class UiActionsExecutionService {
constructor() {}
- async execute({
- action,
- context,
- trigger,
- }: {
- action: Action;
- context: BaseContext;
- trigger: Trigger;
- }): Promise {
+ async execute(
+ {
+ action,
+ context,
+ trigger,
+ }: {
+ action: Action;
+ context: BaseContext;
+ trigger: Trigger;
+ },
+ alwaysShowPopup?: boolean
+ ): Promise {
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,
diff --git a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts
index af2510467ba87..51ba165ba730b 100644
--- a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts
+++ b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts
@@ -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();
diff --git a/src/plugins/ui_actions/public/triggers/index.ts b/src/plugins/ui_actions/public/triggers/index.ts
index b7039d287c6e2..ecbf4d1f7b988 100644
--- a/src/plugins/ui_actions/public/triggers/index.ts
+++ b/src/plugins/ui_actions/public/triggers/index.ts
@@ -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';
diff --git a/src/plugins/ui_actions/public/triggers/row_click_trigger.ts b/src/plugins/ui_actions/public/triggers/row_click_trigger.ts
new file mode 100644
index 0000000000000..87bca03f8c3ba
--- /dev/null
+++ b/src/plugins/ui_actions/public/triggers/row_click_trigger.ts
@@ -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[];
+ };
+}
diff --git a/src/plugins/ui_actions/public/triggers/trigger_contract.ts b/src/plugins/ui_actions/public/triggers/trigger_contract.ts
index ba1c5a693f937..04a75cb3a53d0 100644
--- a/src/plugins/ui_actions/public/triggers/trigger_contract.ts
+++ b/src/plugins/ui_actions/public/triggers/trigger_contract.ts
@@ -49,7 +49,7 @@ export class TriggerContract {
/**
* 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);
};
}
diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts
index c766b5c798ecb..fd43a020504c0 100644
--- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts
+++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts
@@ -31,17 +31,20 @@ export class TriggerInternal {
constructor(public readonly service: UiActionsService, public readonly trigger: Trigger) {}
- 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
+ )
),
]);
}
diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts
index 0be3c19fc1c4d..0266a755be926 100644
--- a/src/plugins/ui_actions/public/types.ts
+++ b/src/plugins/ui_actions/public/types.ts
@@ -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;
diff --git a/src/plugins/visualizations/public/embeddable/events.ts b/src/plugins/visualizations/public/embeddable/events.ts
index 52cac59fbffaa..41e52c3ac1327 100644
--- a/src/plugins/visualizations/public/embeddable/events.ts
+++ b/src/plugins/visualizations/public/embeddable/events.ts
@@ -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,
};
diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts
new file mode 100644
index 0000000000000..e0627c521bb79
--- /dev/null
+++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts
@@ -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 {
+ type = 'test';
+
+ destroy() {}
+ reload() {}
+}
diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts
index 79d380991f5fd..d9f63f233e1c2 100644
--- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts
+++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts
@@ -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);
+ }
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx
index 807dfeed21d1f..3a989c1b0b4cd 100644
--- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx
+++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx
@@ -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 {
embeddable?: IEmbeddable;
}
@@ -65,7 +68,7 @@ export class UrlDrilldown implements Drilldown = ({
@@ -74,12 +77,12 @@ export class UrlDrilldown implements Drilldown {
// 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 (
@@ -93,19 +96,13 @@ export class UrlDrilldown implements Drilldown {
- 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
- 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 {
- 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()];
};
}
diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts
index a93e150deee8f..5917737d15eda 100644
--- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts
+++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts
@@ -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',
+ ],
+ });
});
});
diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts
index 234af380689e9..3e5fc0a968d39 100644
--- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts
+++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts
@@ -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;
- query?: Query;
- filters?: Filter[];
- timeRange?: TimeRange;
- savedObjectId?: string;
+
/**
- * In case panel supports only 1 index patterns
+ * In case panel supports only 1 index pattern.
*/
indexPatternId?: string;
+
/**
- * In case panel supports more then 1 index patterns
+ * In case panel supports more then 1 index pattern.
*/
indexPatternIds?: string[];
-}
-/**
- * Url drilldown context scope
- * `{{context.$}}`
- */
-interface UrlDrilldownContextScope {
- panel?: EmbeddableUrlDrilldownContextScope;
+ query?: Query;
+ filters?: Filter[];
+ timeRange?: TimeRange;
+ savedObjectId?: string;
}
-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): 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
+ ): _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): obj is { savedObjectId: string } {
- return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string';
- }
- function getIndexPatternIds(): string[] {
- function hasIndexPatterns(
- _output: Record
- ): _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>(obj: T): T {
+function deleteUndefinedKeys>(obj: T): T {
Object.keys(obj).forEach((key) => {
if (obj[key] === undefined) {
delete obj[key];
diff --git a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
index 9c7bdc3397f9c..d340d002b242b 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
@@ -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`] = `
+
+
+
+`;
+
exports[`datatable_expression DatatableComponent it renders the title and value 1`] = `
{
).toMatchSnapshot();
});
+ test('it renders actions column when there are row actions', () => {
+ const { data, args } = sampleArgs();
+
+ expect(
+ shallow(
+ 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();
diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
index 6502e07697816..f1eaab908717a 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
@@ -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(
,
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 ;
}
+ 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 (
+
+ {formattedValue}
+
+
+
+ handleFilterClick(field, value, colIndex)}
+ />
+
+
+
+ handleFilterClick(field, value, colIndex, true)}
+ />
+
+
+
+
+
+ );
+ }
+ return {formattedValue};
+ },
+ };
+ })
+ .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 (
{
- 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 (
-
- {formattedValue}
-
-
-
- handleFilterClick(field, value, colIndex)}
- />
-
-
-
- handleFilterClick(field, value, colIndex, true)}
- />
-
-
-
-
-
- );
- }
- return {formattedValue};
- },
- };
- })
- .filter(({ field }) => !!field)}
- items={firstTable ? firstTable.rows : []}
+ columns={tableColumns}
+ items={firstTable ? firstTable.rows.map((row, rowIndex) => ({ ...row, rowIndex })) : []}
/>
);
diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts
index 5d9be46db7fb5..9c7d7ae1f2d43 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/index.ts
+++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts
@@ -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(() =>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
index 54517e4ee8c84..175c573d3be3a 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
@@ -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: '' };
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
index e7d3e1a4bfa5b..6c86ae5cff2c8 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
@@ -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 => {
+ 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() {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
index 65e9c22d24eaf..175ec0dbcfd54 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
@@ -94,6 +94,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
editable: await this.isEditable(),
basePath: coreHttp.basePath,
getTrigger: uiActions?.getTrigger,
+ getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions,
documentToExpression,
},
input,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
index 4645420898314..2fc1cfee82fd3 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
@@ -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 (
@@ -80,6 +83,7 @@ export function ExpressionWrapper({
)}
onEvent={handleEvent}
+ hasCompatibleActions={hasCompatibleActions}
/>
)}
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index b0da6cf2e8434..23d026bf2b443 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -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['data'];
}
+
export interface LensBrushEvent {
name: 'brush';
data: TriggerContext['data'];
}
+export interface LensTableRowContextMenuEvent {
+ name: 'tableRowContextMenuClick';
+ data: TriggerContext['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;
}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
index e6c9797623e9f..1b975da0b369d 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
@@ -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 (
<>
-
+
{JSON.stringify(config)}
>
);
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx
deleted file mode 100644
index a6fcd77d75040..0000000000000
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx
+++ /dev/null
@@ -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();
-
- 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();
-
- 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"`);
-});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
index 3251e85841d86..eb8d01afbf420 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
@@ -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 = ({
config,
+ variables,
onConfig,
- scope,
syntaxHelpDocsLink,
variablesHelpDocsLink,
}) => {
const textAreaRef = useRef(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 = ({
});
}
}
- const { error, isValid } = React.useMemo(
- () => validateUrlTemplate({ template: urlTemplate }, scope),
- [urlTemplate, scope]
- );
const isEmpty = !urlTemplate;
- const isInvalid = !isValid && !isEmpty;
+ const isInvalid = showUrlError && isEmpty;
+ const variablesDropdown = (
+ {
+ if (textAreaRef.current) {
+ updateUrlTemplate(
+ urlTemplate.substr(0, textAreaRef.current!.selectionStart) +
+ `{{${variable}}}` +
+ urlTemplate.substr(textAreaRef.current!.selectionEnd)
+ );
+ } else {
+ updateUrlTemplate(urlTemplate + `{{${variable}}}`);
+ }
+ }}
+ />
+ );
+
return (
<>
= ({
)
}
- labelAppend={
- {
- if (textAreaRef.current) {
- updateUrlTemplate(
- urlTemplate.substr(0, textAreaRef.current!.selectionStart) +
- `{{${variable}}}` +
- urlTemplate.substr(textAreaRef.current!.selectionEnd)
- );
- } else {
- updateUrlTemplate(urlTemplate + `{{${variable}}}`);
- }
- }}
- />
- }
+ labelAppend={variablesDropdown}
>
= ({
value={urlTemplate}
placeholder={txtUrlTemplatePlaceholder}
onChange={(event) => updateUrlTemplate(event.target.value)}
+ onBlur={() => setShowUrlError(true)}
rows={3}
inputRef={textAreaRef}
/>
-
-
- {txtUrlTemplatePreviewLinkText}
-
-
- }
- helpText={txtUrlPreviewHelpText}
- >
-
-
{
- 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",
- ]
- `);
-});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts
deleted file mode 100644
index 74940c4b07077..0000000000000
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts
+++ /dev/null
@@ -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 {
- 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];
-}