mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Row trigger 2 (#83167)
* feat: 🎸 add ROW_CLICK_TRIGGER * feat: 🎸 wire row click event to UI Actions trigger in Lens * feat: 🎸 add row click trigger to url drilldown * feat: 🎸 add datatable to row click context * feat: 🎸 pass in row index in row click trigger context * feat: 🎸 add columns to row click trigger context * feat: 🎸 fill values and keys event scope array * feat: 🎸 generate correct row scope variables * fix: 🐛 report triggers from lens embeddable * feat: 🎸 add sample preview for row click trigger * feat: 🎸 remove url drilldown preview box * chore: 🤖 remove mock variable generation functions * feat: 🎸 generate context and global variable lists * feat: 🎸 preview event variable list * feat: 🎸 show empty url error on blur * feat: 🎸 add ability to always show popup for executed actions * refactor: 💡 rename multiple action execution method * fix: 🐛 don't add separator befor group on no main items * feat: 🎸 wire in uiActions service into datatable renderer * feat: 🎸 check each row if it has compatible row click actions * feat: 🎸 allow passing data to expression renderer * feat: 🎸 add isEmbeddable helper * feat: 🎸 pass embeddable to lens table renderer * feat: 🎸 hide lens table row actions which are empty * feat: 🎸 re-render lens embeddable when dynamic actions chagne * feat: 🎸 hide actions column if there are no row actions * feat: 🎸 re-render lens embeddable on view mode chagne * fix: 🐛 fix TypeScript errors * chore: 🤖 fix TypeScript errors * docs: ✏️ update auto-generated docs * feat: 🎸 add hasCompatibleActions to expression layer * feat: 🎸 remove "data" from expression renderer handlers * fix: 🐛 fix TypeScript errors * test: 💍 fix Jest tests * docs: ✏️ update autogenerated docs * fix: 🐛 wrap event payload into data * test: 💍 add "alwaysShowPopup" test * chore: 🤖 add comment requested in review https://github.com/elastic/kibana/pull/83167#discussion_r537340216 * test: 💍 add hasCompatibleActions test * test: 💍 add datatable renderer test * test: 💍 add Lens embeddable input change tests * test: 💍 add embeddable row click test * fix: 🐛 add url validation * test: 💍 add url drilldown tests * docs: ✏️ remove url drilldown preview from docs * docs: ✏️ remove preview from url templating * docs: ✏️ add row click description * chore: 🤖 move 36.5 KB bundle balance to url_drilldown * test: 💍 simplify test case * style: 💄 change types places * refactor: 💡 clean up panel variable generation * test: 💍 add getPanelVariables() tests * fix: 🐛 generate runtime variables correctly * fix: 🐛 improve getVariableList() and add tests for it * feat: 🎸 add translation, improve types
This commit is contained in:
parent
5a8a5bfd4c
commit
b01a327076
76 changed files with 1687 additions and 587 deletions
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T>;
|
||||
export declare type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T> | RowClickContext;
|
||||
```
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isEmbeddable](./kibana-plugin-plugins-embeddable-public.isembeddable.md)
|
||||
|
||||
## isEmbeddable variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isEmbeddable: (x: unknown) => x is IEmbeddable<import("./i_embeddable").EmbeddableInput, import("./i_embeddable").EmbeddableOutput>
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isRowClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md)
|
||||
|
||||
## isRowClickTriggerContext variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isRowClickTriggerContext: (context: ChartActionContext) => context is RowClickContext
|
||||
```
|
|
@ -78,7 +78,9 @@
|
|||
| [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | |
|
||||
| [EmbeddableRenderer](./kibana-plugin-plugins-embeddable-public.embeddablerenderer.md) | Helper react component to render an embeddable Can be used if you have an embeddable object or an embeddable factory Supports updating input by passing <code>input</code> prop |
|
||||
| [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) | |
|
||||
| [isEmbeddable](./kibana-plugin-plugins-embeddable-public.isembeddable.md) | |
|
||||
| [isRangeSelectTriggerContext](./kibana-plugin-plugins-embeddable-public.israngeselecttriggercontext.md) | |
|
||||
| [isRowClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md) | |
|
||||
| [isValueClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isvalueclicktriggercontext.md) | |
|
||||
| [PANEL\_BADGE\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_badge_trigger.md) | |
|
||||
| [PANEL\_NOTIFICATION\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_notification_trigger.md) | |
|
||||
|
|
|
@ -9,7 +9,7 @@ Constructs a new instance of the `ExpressionRenderHandler` class
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial<ExpressionRenderHandlerParams>);
|
||||
constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -17,5 +17,5 @@ constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial<Expres
|
|||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| element | <code>HTMLElement</code> | |
|
||||
| { onRenderError, renderMode } | <code>Partial<ExpressionRenderHandlerParams></code> | |
|
||||
| { onRenderError, renderMode, hasCompatibleActions, } | <code>ExpressionRenderHandlerParams</code> | |
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export declare class ExpressionRenderHandler
|
|||
|
||||
| Constructor | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [(constructor)(element, { onRenderError, renderMode })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the <code>ExpressionRenderHandler</code> class |
|
||||
| [(constructor)(element, { onRenderError, renderMode, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the <code>ExpressionRenderHandler</code> class |
|
||||
|
||||
## Properties
|
||||
|
||||
|
@ -24,7 +24,7 @@ export declare class ExpressionRenderHandler
|
|||
| [events$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.events_.md) | | <code>Observable<ExpressionRendererEvent></code> | |
|
||||
| [getElement](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.getelement.md) | | <code>() => HTMLElement</code> | |
|
||||
| [handleRenderError](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.handlerendererror.md) | | <code>(error: ExpressionRenderError) => void</code> | |
|
||||
| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | <code>(data: any, uiState?: any) => Promise<void></code> | |
|
||||
| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | <code>(value: any, uiState?: any) => Promise<void></code> | |
|
||||
| [render$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render_.md) | | <code>Observable<number></code> | |
|
||||
| [update$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.update_.md) | | <code>Observable<UpdateValue | null></code> | |
|
||||
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
render: (data: any, uiState?: any) => Promise<void>;
|
||||
render: (value: any, uiState?: any) => Promise<void>;
|
||||
```
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
|
||||
```
|
|
@ -19,6 +19,7 @@ export interface IExpressionLoaderParams
|
|||
| [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | <code>[]</code> | |
|
||||
| [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | <code>boolean</code> | |
|
||||
| [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | <code>boolean</code> | |
|
||||
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | <code>ExpressionRenderHandlerParams['hasCompatibleActions']</code> | |
|
||||
| [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | <code>Adapters</code> | |
|
||||
| [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | <code>RenderErrorHandlerFnType</code> | |
|
||||
| [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | <code>RenderMode</code> | |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasCompatibleActions?: (event: any) => Promise<boolean>;
|
||||
```
|
|
@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers
|
|||
| [done](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.done.md) | <code>() => void</code> | Done increments the number of rendering successes |
|
||||
| [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | <code>(event: any) => void</code> | |
|
||||
| [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md) | <code>() => RenderMode</code> | |
|
||||
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) | <code>(event: any) => Promise<boolean></code> | |
|
||||
| [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | <code>(fn: () => void) => void</code> | |
|
||||
| [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | <code>() => void</code> | |
|
||||
| [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | <code>PersistedState</code> | |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasCompatibleActions?: (event: any) => Promise<boolean>;
|
||||
```
|
|
@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers
|
|||
| [done](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.done.md) | <code>() => void</code> | Done increments the number of rendering successes |
|
||||
| [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | <code>(event: any) => void</code> | |
|
||||
| [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md) | <code>() => RenderMode</code> | |
|
||||
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) | <code>(event: any) => Promise<boolean></code> | |
|
||||
| [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | <code>(fn: () => void) => void</code> | |
|
||||
| [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | <code>() => void</code> | |
|
||||
| [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | <code>PersistedState</code> | |
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
| [Action](./kibana-plugin-plugins-ui_actions-public.action.md) | |
|
||||
| [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) | |
|
||||
| [ActionExecutionMeta](./kibana-plugin-plugins-ui_actions-public.actionexecutionmeta.md) | During action execution we can provide additional information, for example, trigger, that caused the action execution |
|
||||
| [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) | |
|
||||
| [Trigger](./kibana-plugin-plugins-ui_actions-public.trigger.md) | This is a convenience interface used to register a \*trigger\*.<code>Trigger</code> specifies a named anchor to which <code>Action</code> can be attached. When <code>Trigger</code> is being \*called\* it creates a <code>Context</code> object and passes it to the <code>execute</code> method of an <code>Action</code>.<!-- -->More than one action can be attached to a single trigger, in which case when trigger is \*called\* it first displays a context menu for user to pick a single action to execute. |
|
||||
| [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) | |
|
||||
| [UiActionsActionDefinition](./kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md) | A convenience interface used to register an action. |
|
||||
|
@ -42,6 +43,8 @@
|
|||
| [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md) | |
|
||||
| [APPLY\_FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.apply_filter_trigger.md) | |
|
||||
| [applyFilterTrigger](./kibana-plugin-plugins-ui_actions-public.applyfiltertrigger.md) | |
|
||||
| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.row_click_trigger.md) | |
|
||||
| [rowClickTrigger](./kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md) | |
|
||||
| [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.select_range_trigger.md) | |
|
||||
| [selectRangeTrigger](./kibana-plugin-plugins-ui_actions-public.selectrangetrigger.md) | |
|
||||
| [VALUE\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.value_click_trigger.md) | |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
ROW_CLICK_TRIGGER = "ROW_CLICK_TRIGGER"
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
data: {
|
||||
rowIndex: number;
|
||||
table: Datatable;
|
||||
columns?: string[];
|
||||
};
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
embeddable?: IEmbeddable;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface RowClickContext
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [data](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md) | <code>{</code><br/><code> rowIndex: number;</code><br/><code> table: Datatable;</code><br/><code> columns?: string[];</code><br/><code> }</code> | |
|
||||
| [embeddable](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md) | <code>IEmbeddable</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'>
|
||||
```
|
|
@ -16,6 +16,7 @@ export interface TriggerContextMapping
|
|||
| --- | --- | --- |
|
||||
| [""](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md) | <code>TriggerContext</code> | |
|
||||
| [FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.filter_trigger.md) | <code>ApplyGlobalFilterActionContext</code> | |
|
||||
| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md) | <code>RowClickContext</code> | |
|
||||
| [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.select_range_trigger.md) | <code>RangeSelectContext</code> | |
|
||||
| [VALUE\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.value_click_trigger.md) | <code>ValueClickContext</code> | |
|
||||
| [VISUALIZE\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md) | <code>VisualizeFieldContext</code> | |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
[ROW_CLICK_TRIGGER]: RowClickContext;
|
||||
```
|
|
@ -11,5 +11,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
|
||||
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
|
||||
```
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
|
||||
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
|
||||
```
|
||||
|
|
|
@ -12,5 +12,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
|
||||
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
|
||||
```
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
|
||||
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
|
||||
```
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
|
||||
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
|
||||
```
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
|
||||
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
|
||||
```
|
||||
|
|
|
@ -21,17 +21,17 @@ export declare class UiActionsService
|
|||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | <code>ActionRegistry</code> | |
|
||||
| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <code><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</code> | <code>addTriggerAction</code> is similar to <code>attachAction</code> as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.<code>addTriggerAction</code> also infers better typing of the <code>action</code> argument. |
|
||||
| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <code><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</code> | |
|
||||
| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <code><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</code> | <code>addTriggerAction</code> is similar to <code>attachAction</code> as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.<code>addTriggerAction</code> also infers better typing of the <code>action</code> argument. |
|
||||
| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <code><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</code> | |
|
||||
| [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | <code>() => void</code> | Removes all registered triggers and actions. |
|
||||
| [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | <code>(triggerId: TriggerId, actionId: string) => void</code> | |
|
||||
| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <code><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></code> | |
|
||||
| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <code><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></code> | |
|
||||
| [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | <code>UiActionsExecutionService</code> | |
|
||||
| [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | <code>() => UiActionsService</code> | "Fork" a separate instance of <code>UiActionsService</code> that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of <code>UiActionsService</code> are only available within this instance. |
|
||||
| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <code><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"></code> | |
|
||||
| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <code><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></code> | |
|
||||
| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <code><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">[]</code> | |
|
||||
| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <code><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">[]></code> | |
|
||||
| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <code><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></code> | |
|
||||
| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <code><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">[]</code> | |
|
||||
| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <code><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">[]></code> | |
|
||||
| [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | <code>(actionId: string) => boolean</code> | |
|
||||
| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <code><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"></code> | |
|
||||
| [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | <code>(trigger: Trigger) => void</code> | |
|
||||
|
|
|
@ -233,7 +233,7 @@ image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigate
|
|||
https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}}
|
||||
----
|
||||
+
|
||||
The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice. In *URL preview*, `{{event.value}}` is substituted with a <<values-in-preview, dummy>> value.
|
||||
The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice.
|
||||
+
|
||||
[role="screenshot"]
|
||||
image:images/url_drilldown_url_template.png[URL template input]
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
|
@ -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
|
||||
|
|
|
@ -83,10 +83,10 @@ pageLoadAssetSize:
|
|||
transform: 41007
|
||||
triggersActionsUi: 170001
|
||||
uiActions: 97717
|
||||
uiActionsEnhanced: 349511
|
||||
uiActionsEnhanced: 313011
|
||||
upgradeAssistant: 81241
|
||||
uptime: 40825
|
||||
urlDrilldown: 34174
|
||||
urlDrilldown: 70674
|
||||
urlForwarding: 32579
|
||||
usageCollection: 39762
|
||||
visDefaultEditor: 50178
|
||||
|
|
|
@ -54,6 +54,7 @@ export {
|
|||
ErrorEmbeddable,
|
||||
IContainer,
|
||||
IEmbeddable,
|
||||
isEmbeddable,
|
||||
isErrorEmbeddable,
|
||||
openAddPanelFlyout,
|
||||
OutputSpec,
|
||||
|
@ -70,6 +71,7 @@ export {
|
|||
isSavedObjectEmbeddableInput,
|
||||
isRangeSelectTriggerContext,
|
||||
isValueClickTriggerContext,
|
||||
isRowClickTriggerContext,
|
||||
isContextMenuTriggerContext,
|
||||
EmbeddableStateTransfer,
|
||||
EmbeddableEditorState,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Datatable } from '../../../../expressions';
|
||||
import { Trigger } from '../../../../ui_actions/public';
|
||||
import { Trigger, RowClickContext } from '../../../../ui_actions/public';
|
||||
import { IEmbeddable } from '..';
|
||||
|
||||
export interface EmbeddableContext {
|
||||
|
@ -52,7 +52,8 @@ export interface RangeSelectContext<T extends IEmbeddable = IEmbeddable> {
|
|||
|
||||
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> =
|
||||
| ValueClickContext<T>
|
||||
| RangeSelectContext<T>;
|
||||
| RangeSelectContext<T>
|
||||
| RowClickContext;
|
||||
|
||||
export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER';
|
||||
export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = {
|
||||
|
@ -95,6 +96,11 @@ export const isRangeSelectTriggerContext = (
|
|||
context: ChartActionContext
|
||||
): context is RangeSelectContext => context.data && 'range' in context.data;
|
||||
|
||||
export const isRowClickTriggerContext = (context: ChartActionContext): context is RowClickContext =>
|
||||
!!context.data &&
|
||||
typeof context.data === 'object' &&
|
||||
typeof (context as RowClickContext).data.rowIndex === 'number';
|
||||
|
||||
export const isContextMenuTriggerContext = (context: unknown): context is EmbeddableContext =>
|
||||
!!context &&
|
||||
typeof context === 'object' &&
|
||||
|
|
|
@ -176,10 +176,11 @@ export class AttributeService<SavedObjectAttributes extends {
|
|||
wrapAttributes(newAttributes: SavedObjectAttributes, useRefType: boolean, input?: ValType | RefType): Promise<Omit<ValType | RefType, 'id'>>;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "RowClickContext" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ChartActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T>;
|
||||
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> = ValueClickContext<T> | RangeSelectContext<T> | RowClickContext;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Container" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
@ -726,6 +727,11 @@ export interface IEmbeddable<I extends EmbeddableInput = EmbeddableInput, O exte
|
|||
// @public (undocumented)
|
||||
export const isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const isEmbeddable: (x: unknown) => x is IEmbeddable<import("./i_embeddable").EmbeddableInput, import("./i_embeddable").EmbeddableOutput>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isErrorEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
@ -741,6 +747,11 @@ export const isRangeSelectTriggerContext: (context: ChartActionContext) => conte
|
|||
// @public (undocumented)
|
||||
export function isReferenceOrValueEmbeddable(incoming: unknown): incoming is ReferenceOrValueEmbeddable;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isRowClickTriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const isRowClickTriggerContext: (context: ChartActionContext) => context is RowClickContext;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isSavedObjectEmbeddableInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -82,6 +82,7 @@ export interface IInterpreterRenderHandlers {
|
|||
reload: () => void;
|
||||
update: (params: any) => void;
|
||||
event: (event: any) => void;
|
||||
hasCompatibleActions?: (event: any) => Promise<boolean>;
|
||||
getRenderMode: () => RenderMode;
|
||||
uiState?: PersistedState;
|
||||
}
|
||||
|
|
|
@ -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$;
|
||||
|
|
|
@ -532,7 +532,7 @@ export interface ExpressionRenderError extends Error {
|
|||
// @public (undocumented)
|
||||
export class ExpressionRenderHandler {
|
||||
// Warning: (ae-forgotten-export) The symbol "ExpressionRenderHandlerParams" needs to be exported by the entry point index.d.ts
|
||||
constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial<ExpressionRenderHandlerParams>);
|
||||
constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
|
||||
// (undocumented)
|
||||
destroy: () => void;
|
||||
// (undocumented)
|
||||
|
@ -544,7 +544,7 @@ export class ExpressionRenderHandler {
|
|||
// (undocumented)
|
||||
render$: Observable<number>;
|
||||
// (undocumented)
|
||||
render: (data: any, uiState?: any) => Promise<void>;
|
||||
render: (value: any, uiState?: any) => Promise<void>;
|
||||
// Warning: (ae-forgotten-export) The symbol "UpdateValue" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
|
@ -888,6 +888,8 @@ export interface IExpressionLoaderParams {
|
|||
// (undocumented)
|
||||
disableCaching?: boolean;
|
||||
// (undocumented)
|
||||
hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
|
||||
// (undocumented)
|
||||
inspectorAdapters?: Adapters;
|
||||
// Warning: (ae-forgotten-export) The symbol "RenderErrorHandlerFnType" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
|
@ -917,6 +919,8 @@ export interface IInterpreterRenderHandlers {
|
|||
// (undocumented)
|
||||
getRenderMode: () => RenderMode;
|
||||
// (undocumented)
|
||||
hasCompatibleActions?: (event: any) => Promise<boolean>;
|
||||
// (undocumented)
|
||||
onDestroy: (fn: () => void) => void;
|
||||
// (undocumented)
|
||||
reload: () => void;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -29,8 +29,9 @@ import { getRenderersRegistry } from './services';
|
|||
export type IExpressionRendererExtraHandlers = Record<string, any>;
|
||||
|
||||
export interface ExpressionRenderHandlerParams {
|
||||
onRenderError: RenderErrorHandlerFnType;
|
||||
renderMode: RenderMode;
|
||||
onRenderError?: RenderErrorHandlerFnType;
|
||||
renderMode?: RenderMode;
|
||||
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ExpressionRendererEvent {
|
||||
|
@ -59,7 +60,11 @@ export class ExpressionRenderHandler {
|
|||
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
{ onRenderError, renderMode }: Partial<ExpressionRenderHandlerParams> = {}
|
||||
{
|
||||
onRenderError,
|
||||
renderMode,
|
||||
hasCompatibleActions = async () => false,
|
||||
}: ExpressionRenderHandlerParams = {}
|
||||
) {
|
||||
this.element = element;
|
||||
|
||||
|
@ -96,17 +101,18 @@ export class ExpressionRenderHandler {
|
|||
getRenderMode: () => {
|
||||
return renderMode || 'display';
|
||||
},
|
||||
hasCompatibleActions,
|
||||
};
|
||||
}
|
||||
|
||||
render = async (data: any, uiState: any = {}) => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
render = async (value: any, uiState: any = {}) => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return this.handleRenderError(new Error('invalid data provided to the expression renderer'));
|
||||
}
|
||||
|
||||
if (data.type !== 'render' || !data.as) {
|
||||
if (data.type === 'error') {
|
||||
return this.handleRenderError(data.error);
|
||||
if (value.type !== 'render' || !value.as) {
|
||||
if (value.type === 'error') {
|
||||
return this.handleRenderError(value.error);
|
||||
} else {
|
||||
return this.handleRenderError(
|
||||
new Error('invalid data provided to the expression renderer')
|
||||
|
@ -114,15 +120,15 @@ export class ExpressionRenderHandler {
|
|||
}
|
||||
}
|
||||
|
||||
if (!getRenderersRegistry().get(data.as)) {
|
||||
return this.handleRenderError(new Error(`invalid renderer id '${data.as}'`));
|
||||
if (!getRenderersRegistry().get(value.as)) {
|
||||
return this.handleRenderError(new Error(`invalid renderer id '${value.as}'`));
|
||||
}
|
||||
|
||||
try {
|
||||
// Rendering is asynchronous, completed by handlers.done()
|
||||
await getRenderersRegistry()
|
||||
.get(data.as)!
|
||||
.render(this.element, data.value, {
|
||||
.get(value.as)!
|
||||
.render(this.element, value.value, {
|
||||
...this.handlers,
|
||||
uiState,
|
||||
} as any);
|
||||
|
@ -152,7 +158,7 @@ export class ExpressionRenderHandler {
|
|||
export function render(
|
||||
element: HTMLElement,
|
||||
data: any,
|
||||
options?: Partial<ExpressionRenderHandlerParams>
|
||||
options?: ExpressionRenderHandlerParams
|
||||
): ExpressionRenderHandler {
|
||||
const handler = new ExpressionRenderHandler(element, options);
|
||||
handler.render(data);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -736,6 +736,8 @@ export interface IInterpreterRenderHandlers {
|
|||
// (undocumented)
|
||||
getRenderMode: () => RenderMode;
|
||||
// (undocumented)
|
||||
hasCompatibleActions?: (event: any) => Promise<boolean>;
|
||||
// (undocumented)
|
||||
onDestroy: (fn: () => void) => void;
|
||||
// (undocumented)
|
||||
reload: () => void;
|
||||
|
|
|
@ -50,6 +50,9 @@ export {
|
|||
visualizeFieldTrigger,
|
||||
VISUALIZE_GEO_FIELD_TRIGGER,
|
||||
visualizeGeoFieldTrigger,
|
||||
ROW_CLICK_TRIGGER,
|
||||
rowClickTrigger,
|
||||
RowClickContext,
|
||||
} from './triggers';
|
||||
export {
|
||||
TriggerContextMapping,
|
||||
|
|
|
@ -23,6 +23,7 @@ import { UiActionsService } from './service';
|
|||
import {
|
||||
selectRangeTrigger,
|
||||
valueClickTrigger,
|
||||
rowClickTrigger,
|
||||
applyFilterTrigger,
|
||||
visualizeFieldTrigger,
|
||||
visualizeGeoFieldTrigger,
|
||||
|
@ -48,6 +49,7 @@ export class UiActionsPlugin implements Plugin<UiActionsSetup, UiActionsStart> {
|
|||
public setup(core: CoreSetup): UiActionsSetup {
|
||||
this.service.registerTrigger(selectRangeTrigger);
|
||||
this.service.registerTrigger(valueClickTrigger);
|
||||
this.service.registerTrigger(rowClickTrigger);
|
||||
this.service.registerTrigger(applyFilterTrigger);
|
||||
this.service.registerTrigger(visualizeFieldTrigger);
|
||||
this.service.registerTrigger(visualizeGeoFieldTrigger);
|
||||
|
|
|
@ -133,6 +133,32 @@ export class IncompatibleActionError extends Error {
|
|||
// @public (undocumented)
|
||||
export function plugin(initializerContext: PluginInitializerContext): UiActionsPlugin;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ROW_CLICK_TRIGGER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const ROW_CLICK_TRIGGER = "ROW_CLICK_TRIGGER";
|
||||
|
||||
// Warning: (ae-missing-release-tag) "RowClickContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface RowClickContext {
|
||||
// (undocumented)
|
||||
data: {
|
||||
rowIndex: number;
|
||||
table: Datatable;
|
||||
columns?: string[];
|
||||
};
|
||||
// Warning: (ae-forgotten-export) The symbol "IEmbeddable" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
embeddable?: IEmbeddable;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "rowClickTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SELECT_RANGE_TRIGGER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
@ -170,6 +196,8 @@ export interface TriggerContextMapping {
|
|||
//
|
||||
// (undocumented)
|
||||
[APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext;
|
||||
// (undocumented)
|
||||
[ROW_CLICK_TRIGGER]: RowClickContext;
|
||||
// Warning: (ae-forgotten-export) The symbol "RangeSelectContext" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
|
@ -234,14 +262,14 @@ export class UiActionsService {
|
|||
//
|
||||
// (undocumented)
|
||||
protected readonly actions: ActionRegistry;
|
||||
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: UiActionsActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
|
||||
readonly addTriggerAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: UiActionsActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void;
|
||||
// (undocumented)
|
||||
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
|
||||
readonly attachAction: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void;
|
||||
readonly clear: () => void;
|
||||
// (undocumented)
|
||||
readonly detachAction: (triggerId: TriggerId, actionId: string) => void;
|
||||
// @deprecated (undocumented)
|
||||
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
|
||||
readonly executeTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void>;
|
||||
// Warning: (ae-forgotten-export) The symbol "UiActionsExecutionService" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
|
@ -252,11 +280,11 @@ export class UiActionsService {
|
|||
// Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
|
||||
readonly getTrigger: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T>;
|
||||
// (undocumented)
|
||||
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
|
||||
readonly getTriggerActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[];
|
||||
// (undocumented)
|
||||
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
|
||||
readonly getTriggerCompatibleActions: <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]>;
|
||||
// (undocumented)
|
||||
readonly hasAction: (actionId: string) => boolean;
|
||||
// Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts
|
||||
|
@ -341,6 +369,10 @@ export const visualizeFieldTrigger: Trigger<'VISUALIZE_FIELD_TRIGGER'>;
|
|||
export const visualizeGeoFieldTrigger: Trigger<'VISUALIZE_GEO_FIELD_TRIGGER'>;
|
||||
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/plugins/ui_actions/public/triggers/row_click_trigger.ts:45:5 - (ae-forgotten-export) The symbol "Datatable" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
```
|
||||
|
|
|
@ -29,6 +29,7 @@ interface ExecuteActionTask {
|
|||
context: BaseContext;
|
||||
trigger: Trigger;
|
||||
defer: Defer<void>;
|
||||
alwaysShowPopup?: boolean;
|
||||
}
|
||||
|
||||
export class UiActionsExecutionService {
|
||||
|
@ -37,21 +38,25 @@ export class UiActionsExecutionService {
|
|||
|
||||
constructor() {}
|
||||
|
||||
async execute({
|
||||
action,
|
||||
context,
|
||||
trigger,
|
||||
}: {
|
||||
action: Action<BaseContext>;
|
||||
context: BaseContext;
|
||||
trigger: Trigger;
|
||||
}): Promise<void> {
|
||||
async execute(
|
||||
{
|
||||
action,
|
||||
context,
|
||||
trigger,
|
||||
}: {
|
||||
action: Action<BaseContext>;
|
||||
context: BaseContext;
|
||||
trigger: Trigger;
|
||||
},
|
||||
alwaysShowPopup?: boolean
|
||||
): Promise<void> {
|
||||
const shouldBatch = !(await action.shouldAutoExecute?.({ ...context, trigger })) ?? false;
|
||||
const task: ExecuteActionTask = {
|
||||
action,
|
||||
context,
|
||||
trigger,
|
||||
defer: createDefer(),
|
||||
alwaysShowPopup: !!alwaysShowPopup,
|
||||
};
|
||||
|
||||
if (shouldBatch) {
|
||||
|
@ -84,11 +89,23 @@ export class UiActionsExecutionService {
|
|||
setTimeout(() => {
|
||||
if (this.pendingTasks.size === 0) {
|
||||
const tasks = uniqBy(this.batchingQueue, (t) => t.action.id);
|
||||
if (tasks.length === 1) {
|
||||
this.executeSingleTask(tasks[0]);
|
||||
}
|
||||
if (tasks.length > 1) {
|
||||
this.executeMultipleActions(tasks);
|
||||
if (tasks.length > 0) {
|
||||
let alwaysShowPopup = false;
|
||||
for (const task of tasks) {
|
||||
if (task.alwaysShowPopup) {
|
||||
alwaysShowPopup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alwaysShowPopup) {
|
||||
this.showActionPopupMenu(tasks);
|
||||
} else {
|
||||
if (tasks.length === 1) {
|
||||
this.executeSingleTask(tasks[0]);
|
||||
} else if (tasks.length > 1) {
|
||||
this.showActionPopupMenu(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.batchingQueue.splice(0, this.batchingQueue.length);
|
||||
|
@ -108,7 +125,7 @@ export class UiActionsExecutionService {
|
|||
}
|
||||
}
|
||||
|
||||
private async executeMultipleActions(tasks: ExecuteActionTask[]) {
|
||||
private async showActionPopupMenu(tasks: ExecuteActionTask[]) {
|
||||
const panels = await buildContextMenuForActions({
|
||||
actions: tasks.map(({ action, context, trigger }) => ({
|
||||
action,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
53
src/plugins/ui_actions/public/triggers/row_click_trigger.ts
Normal file
53
src/plugins/ui_actions/public/triggers/row_click_trigger.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IEmbeddable } from '../../../embeddable/public';
|
||||
import { Trigger } from '.';
|
||||
import { Datatable } from '../../../expressions';
|
||||
|
||||
export const ROW_CLICK_TRIGGER = 'ROW_CLICK_TRIGGER';
|
||||
|
||||
export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'> = {
|
||||
id: ROW_CLICK_TRIGGER,
|
||||
title: i18n.translate('uiActions.triggers.rowClickTitle', {
|
||||
defaultMessage: 'Table row click',
|
||||
}),
|
||||
description: i18n.translate('uiActions.triggers.rowClickkDescription', {
|
||||
defaultMessage: 'A click on a table row',
|
||||
}),
|
||||
};
|
||||
|
||||
export interface RowClickContext {
|
||||
embeddable?: IEmbeddable;
|
||||
data: {
|
||||
/**
|
||||
* Row index, starting from 0, where user clicked.
|
||||
*/
|
||||
rowIndex: number;
|
||||
|
||||
table: Datatable;
|
||||
|
||||
/**
|
||||
* Sorted list column IDs that were visible to the user. Useful when only
|
||||
* a subset of datatable columns should be used.
|
||||
*/
|
||||
columns?: string[];
|
||||
};
|
||||
}
|
|
@ -49,7 +49,7 @@ export class TriggerContract<T extends TriggerId> {
|
|||
/**
|
||||
* Use this method to execute action attached to this trigger.
|
||||
*/
|
||||
public readonly exec = async (context: TriggerContextMapping[T]) => {
|
||||
await this.internal.execute(context);
|
||||
public readonly exec = async (context: TriggerContextMapping[T], alwaysShowPopup?: boolean) => {
|
||||
await this.internal.execute(context, alwaysShowPopup);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,17 +31,20 @@ export class TriggerInternal<T extends TriggerId> {
|
|||
|
||||
constructor(public readonly service: UiActionsService, public readonly trigger: Trigger<T>) {}
|
||||
|
||||
public async execute(context: TriggerContextMapping[T]) {
|
||||
public async execute(context: TriggerContextMapping[T], alwaysShowPopup?: boolean) {
|
||||
const triggerId = this.trigger.id;
|
||||
const actions = await this.service.getTriggerCompatibleActions!(triggerId, context);
|
||||
|
||||
await Promise.all([
|
||||
actions.map((action) =>
|
||||
this.service.executionService.execute({
|
||||
action,
|
||||
context,
|
||||
trigger: this.trigger,
|
||||
})
|
||||
this.service.executionService.execute(
|
||||
{
|
||||
action,
|
||||
context,
|
||||
trigger: this.trigger,
|
||||
},
|
||||
alwaysShowPopup
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
173
x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts
Normal file
173
x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { DatatableColumnType } from '../../../../../../../src/plugins/expressions/common';
|
||||
import {
|
||||
Embeddable,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
} from '../../../../../../../src/plugins/embeddable/public';
|
||||
|
||||
export const createPoint = ({
|
||||
field,
|
||||
value,
|
||||
}: {
|
||||
field: string;
|
||||
value: string | null | number | boolean;
|
||||
}) => ({
|
||||
table: {
|
||||
columns: [
|
||||
{
|
||||
name: field,
|
||||
id: '1-1',
|
||||
meta: {
|
||||
type: 'date' as DatatableColumnType,
|
||||
field,
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
type: 'histogram',
|
||||
indexPatternId: 'logstash-*',
|
||||
interval: 30,
|
||||
otherBucket: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
'1-1': '2048',
|
||||
},
|
||||
],
|
||||
},
|
||||
column: 0,
|
||||
row: 0,
|
||||
value,
|
||||
});
|
||||
|
||||
export const rowClickData = {
|
||||
rowIndex: 1,
|
||||
table: {
|
||||
type: 'datatable',
|
||||
rows: [
|
||||
{
|
||||
'6ced5344-2596-4545-b626-8b449924e2d4': 'IT',
|
||||
'6890e417-c5f1-4565-a45c-92f55380e14c': '0',
|
||||
'93b8ef16-2483-45b8-ad27-6cc1f790578b': 13,
|
||||
'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 0,
|
||||
'e0719f1a-04fb-4036-a63c-c25deac3f011': 7,
|
||||
},
|
||||
{
|
||||
'6ced5344-2596-4545-b626-8b449924e2d4': 'IT',
|
||||
'6890e417-c5f1-4565-a45c-92f55380e14c': '2.25',
|
||||
'93b8ef16-2483-45b8-ad27-6cc1f790578b': 3,
|
||||
'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 0,
|
||||
'e0719f1a-04fb-4036-a63c-c25deac3f011': 2,
|
||||
},
|
||||
{
|
||||
'6ced5344-2596-4545-b626-8b449924e2d4': 'IT',
|
||||
'6890e417-c5f1-4565-a45c-92f55380e14c': '0.020939215995129826',
|
||||
'93b8ef16-2483-45b8-ad27-6cc1f790578b': 2,
|
||||
'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 12.490584373474121,
|
||||
'e0719f1a-04fb-4036-a63c-c25deac3f011': 1,
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
id: '6ced5344-2596-4545-b626-8b449924e2d4',
|
||||
name: 'Top values of DestCountry',
|
||||
meta: {
|
||||
type: 'string',
|
||||
field: 'DestCountry',
|
||||
index: 'kibana_sample_data_flights',
|
||||
params: {
|
||||
id: 'terms',
|
||||
params: {
|
||||
id: 'string',
|
||||
otherBucketLabel: 'Other',
|
||||
missingBucketLabel: '(missing value)',
|
||||
},
|
||||
},
|
||||
source: 'esaggs',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6890e417-c5f1-4565-a45c-92f55380e14c',
|
||||
name: 'Top values of FlightTimeHour',
|
||||
meta: {
|
||||
type: 'string',
|
||||
field: 'FlightTimeHour',
|
||||
index: 'kibana_sample_data_flights',
|
||||
params: {
|
||||
id: 'terms',
|
||||
params: {
|
||||
id: 'string',
|
||||
otherBucketLabel: 'Other',
|
||||
missingBucketLabel: '(missing value)',
|
||||
},
|
||||
},
|
||||
source: 'esaggs',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '93b8ef16-2483-45b8-ad27-6cc1f790578b',
|
||||
name: 'Count of records',
|
||||
meta: {
|
||||
type: 'number',
|
||||
index: 'kibana_sample_data_flights',
|
||||
params: {
|
||||
id: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0c5dcc2-4012-4d7e-b983-0e089badc43c',
|
||||
name: 'Average of DistanceMiles',
|
||||
meta: {
|
||||
type: 'number',
|
||||
field: 'DistanceMiles',
|
||||
index: 'kibana_sample_data_flights',
|
||||
params: {
|
||||
id: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e0719f1a-04fb-4036-a63c-c25deac3f011',
|
||||
name: 'Unique count of OriginAirportID',
|
||||
meta: {
|
||||
type: 'string',
|
||||
field: 'OriginAirportID',
|
||||
index: 'kibana_sample_data_flights',
|
||||
params: {
|
||||
id: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
'6ced5344-2596-4545-b626-8b449924e2d4',
|
||||
'6890e417-c5f1-4565-a45c-92f55380e14c',
|
||||
'93b8ef16-2483-45b8-ad27-6cc1f790578b',
|
||||
'b0c5dcc2-4012-4d7e-b983-0e089badc43c',
|
||||
'e0719f1a-04fb-4036-a63c-c25deac3f011',
|
||||
],
|
||||
};
|
||||
|
||||
interface TestInput extends EmbeddableInput {
|
||||
savedObjectId?: string;
|
||||
}
|
||||
|
||||
interface TestOutput extends EmbeddableOutput {
|
||||
indexPatterns?: Array<{ id: string }>;
|
||||
}
|
||||
|
||||
export class TestEmbeddable extends Embeddable<TestInput, TestOutput> {
|
||||
type = 'test';
|
||||
|
||||
destroy() {}
|
||||
reload() {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { getFlattenedObject } from '@kbn/std';
|
||||
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
ChartActionContext,
|
||||
|
@ -13,6 +14,7 @@ import {
|
|||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
import {
|
||||
ROW_CLICK_TRIGGER,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '../../../../../../src/plugins/ui_actions/public';
|
||||
|
@ -22,11 +24,10 @@ import {
|
|||
UrlDrilldownConfig,
|
||||
UrlDrilldownCollectConfig,
|
||||
urlDrilldownValidateUrlTemplate,
|
||||
urlDrilldownBuildScope,
|
||||
urlDrilldownCompileUrl,
|
||||
UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
|
||||
} from '../../../../ui_actions_enhanced/public';
|
||||
import { getContextScope, getEventScope, getMockEventScope } from './url_drilldown_scope';
|
||||
import { getPanelVariables, getEventScope, getEventVariableList } from './url_drilldown_scope';
|
||||
import { txtUrlDrilldownDisplayName } from './i18n';
|
||||
|
||||
interface UrlDrilldownDeps {
|
||||
|
@ -39,9 +40,11 @@ interface UrlDrilldownDeps {
|
|||
export type ActionContext = ChartActionContext;
|
||||
export type Config = UrlDrilldownConfig;
|
||||
export type UrlTrigger =
|
||||
| typeof CONTEXT_MENU_TRIGGER
|
||||
| typeof VALUE_CLICK_TRIGGER
|
||||
| typeof SELECT_RANGE_TRIGGER;
|
||||
| typeof SELECT_RANGE_TRIGGER
|
||||
| typeof ROW_CLICK_TRIGGER
|
||||
| typeof CONTEXT_MENU_TRIGGER;
|
||||
|
||||
export interface ActionFactoryContext extends BaseActionFactoryContext<UrlTrigger> {
|
||||
embeddable?: IEmbeddable;
|
||||
}
|
||||
|
@ -65,7 +68,7 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
|
|||
public readonly euiIcon = 'link';
|
||||
|
||||
supportedTriggers(): UrlTrigger[] {
|
||||
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, CONTEXT_MENU_TRIGGER];
|
||||
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, ROW_CLICK_TRIGGER, CONTEXT_MENU_TRIGGER];
|
||||
}
|
||||
|
||||
private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({
|
||||
|
@ -74,12 +77,12 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
|
|||
context,
|
||||
}) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const scope = React.useMemo(() => this.buildEditorScope(context), [context]);
|
||||
const variables = React.useMemo(() => this.getVariableList(context), [context]);
|
||||
return (
|
||||
<UrlDrilldownCollectConfig
|
||||
variables={variables}
|
||||
config={config}
|
||||
onConfig={onConfig}
|
||||
scope={scope}
|
||||
syntaxHelpDocsLink={this.deps.getSyntaxHelpDocsLink()}
|
||||
variablesHelpDocsLink={this.deps.getVariablesHelpDocsLink()}
|
||||
/>
|
||||
|
@ -93,19 +96,13 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
|
|||
openInNewTab: false,
|
||||
});
|
||||
|
||||
public readonly isConfigValid = (
|
||||
config: Config,
|
||||
context: ActionFactoryContext
|
||||
): config is Config => {
|
||||
const { isValid } = urlDrilldownValidateUrlTemplate(config.url, this.buildEditorScope(context));
|
||||
return isValid;
|
||||
public readonly isConfigValid = (config: Config): config is Config => {
|
||||
return !!config.url.template;
|
||||
};
|
||||
|
||||
public readonly isCompatible = async (config: Config, context: ActionContext) => {
|
||||
const { isValid, error } = urlDrilldownValidateUrlTemplate(
|
||||
config.url,
|
||||
await this.buildRuntimeScope(context)
|
||||
);
|
||||
const scope = this.getRuntimeVariables(context);
|
||||
const { isValid, error } = urlDrilldownValidateUrlTemplate(config.url, scope);
|
||||
|
||||
if (!isValid) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -117,11 +114,13 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
|
|||
return Promise.resolve(isValid);
|
||||
};
|
||||
|
||||
public readonly getHref = async (config: Config, context: ActionContext) =>
|
||||
urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context));
|
||||
public readonly getHref = async (config: Config, context: ActionContext) => {
|
||||
const scope = this.getRuntimeVariables(context);
|
||||
return urlDrilldownCompileUrl(config.url.template, scope);
|
||||
};
|
||||
|
||||
public readonly execute = async (config: Config, context: ActionContext) => {
|
||||
const url = urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context));
|
||||
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
|
||||
if (config.openInNewTab) {
|
||||
window.open(url, '_blank', 'noopener');
|
||||
} else {
|
||||
|
@ -129,19 +128,23 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
|
|||
}
|
||||
};
|
||||
|
||||
private buildEditorScope = (context: ActionFactoryContext) => {
|
||||
return urlDrilldownBuildScope({
|
||||
globalScope: this.deps.getGlobalScope(),
|
||||
contextScope: getContextScope(context),
|
||||
eventScope: getMockEventScope(context.triggers),
|
||||
});
|
||||
public readonly getRuntimeVariables = (context: ActionContext) => {
|
||||
return {
|
||||
...this.deps.getGlobalScope(),
|
||||
context: {
|
||||
panel: getPanelVariables(context),
|
||||
},
|
||||
event: getEventScope(context),
|
||||
};
|
||||
};
|
||||
|
||||
private buildRuntimeScope = (context: ActionContext) => {
|
||||
return urlDrilldownBuildScope({
|
||||
globalScope: this.deps.getGlobalScope(),
|
||||
contextScope: getContextScope(context),
|
||||
eventScope: getEventScope(context),
|
||||
});
|
||||
public readonly getVariableList = (context: ActionFactoryContext): string[] => {
|
||||
const eventVariables = getEventVariableList(context);
|
||||
const contextVariables = Object.keys(getFlattenedObject(getPanelVariables(context))).map(
|
||||
(key) => 'context.panel.' + key
|
||||
);
|
||||
const globalVariables = Object.keys(getFlattenedObject(this.deps.getGlobalScope()));
|
||||
|
||||
return [...eventVariables.sort(), ...contextVariables.sort(), ...globalVariables.sort()];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,48 +14,54 @@ import {
|
|||
IEmbeddable,
|
||||
isRangeSelectTriggerContext,
|
||||
isValueClickTriggerContext,
|
||||
isRowClickTriggerContext,
|
||||
isContextMenuTriggerContext,
|
||||
RangeSelectContext,
|
||||
ValueClickContext,
|
||||
EmbeddableOutput,
|
||||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown';
|
||||
import type { ActionContext, ActionFactoryContext } from './url_drilldown';
|
||||
import {
|
||||
SELECT_RANGE_TRIGGER,
|
||||
RowClickContext,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
ROW_CLICK_TRIGGER,
|
||||
} from '../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
type ContextScopeInput = ActionContext | ActionFactoryContext;
|
||||
|
||||
/**
|
||||
* Part of context scope extracted from an embeddable
|
||||
* Expose on the scope as: `{{context.panel.id}}`, `{{context.panel.filters.[0]}}`
|
||||
*/
|
||||
interface EmbeddableUrlDrilldownContextScope {
|
||||
/**
|
||||
* ID of the embeddable panel.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Title of the embeddable panel.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* In case panel supports only 1 index pattern.
|
||||
*/
|
||||
indexPatternId?: string;
|
||||
|
||||
/**
|
||||
* In case panel supports more then 1 index pattern.
|
||||
*/
|
||||
indexPatternIds?: string[];
|
||||
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
savedObjectId?: string;
|
||||
/**
|
||||
* In case panel supports only 1 index patterns
|
||||
*/
|
||||
indexPatternId?: string;
|
||||
/**
|
||||
* In case panel supports more then 1 index patterns
|
||||
*/
|
||||
indexPatternIds?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Url drilldown context scope
|
||||
* `{{context.$}}`
|
||||
*/
|
||||
interface UrlDrilldownContextScope {
|
||||
panel?: EmbeddableUrlDrilldownContextScope;
|
||||
}
|
||||
|
||||
export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilldownContextScope {
|
||||
export function getPanelVariables(contextScopeInput: {
|
||||
embeddable?: IEmbeddable;
|
||||
}): EmbeddableUrlDrilldownContextScope {
|
||||
function hasEmbeddable(val: unknown): val is { embeddable: IEmbeddable } {
|
||||
if (val && typeof val === 'object' && 'embeddable' in val) return true;
|
||||
return false;
|
||||
|
@ -64,41 +70,52 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld
|
|||
throw new Error(
|
||||
"UrlDrilldown [getContextScope] can't build scope because embeddable object is missing in context"
|
||||
);
|
||||
|
||||
const embeddable = contextScopeInput.embeddable;
|
||||
|
||||
return getEmbeddableVariables(embeddable);
|
||||
}
|
||||
|
||||
function hasSavedObjectId(obj: Record<string, any>): obj is { savedObjectId: string } {
|
||||
return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Same functionality is implemented in x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts,
|
||||
* combine both implementations into a common approach.
|
||||
*/
|
||||
function getIndexPatternIds(output: EmbeddableOutput): string[] {
|
||||
function hasIndexPatterns(
|
||||
_output: Record<string, any>
|
||||
): _output is { indexPatterns: Array<{ id?: string }> } {
|
||||
return (
|
||||
'indexPatterns' in _output &&
|
||||
Array.isArray(_output.indexPatterns) &&
|
||||
_output.indexPatterns.length > 0
|
||||
);
|
||||
}
|
||||
return hasIndexPatterns(output)
|
||||
? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[])
|
||||
: [];
|
||||
}
|
||||
|
||||
export function getEmbeddableVariables(
|
||||
embeddable: IEmbeddable
|
||||
): EmbeddableUrlDrilldownContextScope {
|
||||
const input = embeddable.getInput();
|
||||
const output = embeddable.getOutput();
|
||||
function hasSavedObjectId(obj: Record<string, any>): obj is { savedObjectId: string } {
|
||||
return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string';
|
||||
}
|
||||
function getIndexPatternIds(): string[] {
|
||||
function hasIndexPatterns(
|
||||
_output: Record<string, any>
|
||||
): _output is { indexPatterns: Array<{ id?: string }> } {
|
||||
return (
|
||||
'indexPatterns' in _output &&
|
||||
Array.isArray(_output.indexPatterns) &&
|
||||
_output.indexPatterns.length > 0
|
||||
);
|
||||
}
|
||||
return hasIndexPatterns(output)
|
||||
? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[])
|
||||
: [];
|
||||
}
|
||||
const indexPatternsIds = getIndexPatternIds();
|
||||
return {
|
||||
panel: cleanEmptyKeys({
|
||||
id: input.id,
|
||||
title: output.title ?? input.title,
|
||||
savedObjectId:
|
||||
output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined),
|
||||
query: input.query,
|
||||
timeRange: input.timeRange,
|
||||
filters: input.filters,
|
||||
indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined,
|
||||
indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined,
|
||||
}),
|
||||
};
|
||||
const indexPatternsIds = getIndexPatternIds(output);
|
||||
|
||||
return deleteUndefinedKeys({
|
||||
id: input.id,
|
||||
title: output.title ?? input.title,
|
||||
savedObjectId:
|
||||
output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined),
|
||||
query: input.query,
|
||||
timeRange: input.timeRange,
|
||||
filters: input.filters,
|
||||
indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined,
|
||||
indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -108,7 +125,9 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld
|
|||
export type UrlDrilldownEventScope =
|
||||
| ValueClickTriggerEventScope
|
||||
| RangeSelectTriggerEventScope
|
||||
| RowClickTriggerEventScope
|
||||
| ContextMenuTriggerEventScope;
|
||||
|
||||
export type EventScopeInput = ActionContext;
|
||||
export interface ValueClickTriggerEventScope {
|
||||
key?: string;
|
||||
|
@ -122,6 +141,12 @@ export interface RangeSelectTriggerEventScope {
|
|||
to?: string | number;
|
||||
}
|
||||
|
||||
export interface RowClickTriggerEventScope {
|
||||
rowIndex: number;
|
||||
values: Primitive[];
|
||||
keys: string[];
|
||||
columnNames: string[];
|
||||
}
|
||||
export type ContextMenuTriggerEventScope = object;
|
||||
|
||||
export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope {
|
||||
|
@ -129,6 +154,8 @@ export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEve
|
|||
return getEventScopeFromRangeSelectTriggerContext(eventScopeInput);
|
||||
} else if (isValueClickTriggerContext(eventScopeInput)) {
|
||||
return getEventScopeFromValueClickTriggerContext(eventScopeInput);
|
||||
} else if (isRowClickTriggerContext(eventScopeInput)) {
|
||||
return getEventScopeFromRowClickTriggerContext(eventScopeInput);
|
||||
} else if (isContextMenuTriggerContext(eventScopeInput)) {
|
||||
return {};
|
||||
} else {
|
||||
|
@ -141,7 +168,7 @@ function getEventScopeFromRangeSelectTriggerContext(
|
|||
): RangeSelectTriggerEventScope {
|
||||
const { table, column: columnIndex, range } = eventScopeInput.data;
|
||||
const column = table.columns[columnIndex];
|
||||
return cleanEmptyKeys({
|
||||
return deleteUndefinedKeys({
|
||||
key: toPrimitiveOrUndefined(column?.meta.field) as string,
|
||||
from: toPrimitiveOrUndefined(range[0]) as string | number | undefined,
|
||||
to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined,
|
||||
|
@ -160,7 +187,7 @@ function getEventScopeFromValueClickTriggerContext(
|
|||
};
|
||||
});
|
||||
|
||||
return cleanEmptyKeys({
|
||||
return deleteUndefinedKeys({
|
||||
key: points[0]?.key,
|
||||
value: points[0]?.value,
|
||||
negate,
|
||||
|
@ -168,37 +195,53 @@ function getEventScopeFromValueClickTriggerContext(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @remarks
|
||||
* Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel)
|
||||
* `event` variables are mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL
|
||||
*/
|
||||
export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventScope {
|
||||
if (trigger === SELECT_RANGE_TRIGGER) {
|
||||
return {
|
||||
key: 'event.key',
|
||||
from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago
|
||||
to: new Date().toISOString(),
|
||||
};
|
||||
function getEventScopeFromRowClickTriggerContext({
|
||||
embeddable,
|
||||
data,
|
||||
}: RowClickContext): RowClickTriggerEventScope {
|
||||
const { rowIndex } = data;
|
||||
const columns = data.columns || data.table.columns.map(({ id }) => id);
|
||||
const values: Primitive[] = [];
|
||||
const keys: string[] = [];
|
||||
const columnNames: string[] = [];
|
||||
const row = data.table.rows[rowIndex];
|
||||
|
||||
for (const columnId of columns) {
|
||||
const column = data.table.columns.find(({ id }) => id === columnId);
|
||||
if (!column) {
|
||||
// This should never happe, but in case it does we log data necessary for debugging.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(data, embeddable ? `Embeddable [${embeddable.getTitle()}]` : null);
|
||||
throw new Error('Could not find a datatable column.');
|
||||
}
|
||||
values.push(row[columnId]);
|
||||
keys.push(column.meta.field || '');
|
||||
columnNames.push(column.name || column.meta.field || '');
|
||||
}
|
||||
|
||||
if (trigger === VALUE_CLICK_TRIGGER) {
|
||||
// number of mock points to generate
|
||||
// should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER
|
||||
const nPoints = 4;
|
||||
const points = new Array(nPoints).fill(0).map((_, index) => ({
|
||||
key: `event.points.${index}.key`,
|
||||
value: `event.points.${index}.value`,
|
||||
}));
|
||||
return {
|
||||
key: `event.key`,
|
||||
value: `event.value`,
|
||||
negate: false,
|
||||
points,
|
||||
};
|
||||
const scope: RowClickTriggerEventScope = {
|
||||
rowIndex,
|
||||
values,
|
||||
keys,
|
||||
columnNames,
|
||||
};
|
||||
|
||||
return scope;
|
||||
}
|
||||
|
||||
export function getEventVariableList(context: ActionFactoryContext): string[] {
|
||||
const [trigger] = context.triggers;
|
||||
|
||||
switch (trigger) {
|
||||
case SELECT_RANGE_TRIGGER:
|
||||
return ['event.key', 'event.from', 'event.to'];
|
||||
case VALUE_CLICK_TRIGGER:
|
||||
return ['event.key', 'event.value', 'event.negate', 'event.points'];
|
||||
case ROW_CLICK_TRIGGER:
|
||||
return ['event.rowIndex', 'event.values', 'event.keys', 'event.columnNames'];
|
||||
}
|
||||
|
||||
return {};
|
||||
return [];
|
||||
}
|
||||
|
||||
type Primitive = string | number | boolean | null;
|
||||
|
@ -210,7 +253,7 @@ function toPrimitiveOrUndefined(v: unknown): Primitive | undefined {
|
|||
return String(v);
|
||||
}
|
||||
|
||||
function cleanEmptyKeys<T extends Record<string, any>>(obj: T): T {
|
||||
function deleteUndefinedKeys<T extends Record<string, any>>(obj: T): T {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (obj[key] === undefined) {
|
||||
delete obj[key];
|
||||
|
|
|
@ -1,5 +1,60 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`datatable_expression DatatableComponent it renders actions column when there are row actions 1`] = `
|
||||
<VisualizationContainer
|
||||
reportTitle="My fanci metric chart"
|
||||
>
|
||||
<EuiBasicTable
|
||||
className="lnsDataTable"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "a",
|
||||
"name": "a",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "b",
|
||||
"name": "b",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "c",
|
||||
"name": "c",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"description": "Table row context menu",
|
||||
"icon": [Function],
|
||||
"name": "More",
|
||||
"onClick": [Function],
|
||||
"type": "icon",
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
data-test-subj="lnsDataTable"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"a": "shoes",
|
||||
"b": 1588024800000,
|
||||
"c": 3,
|
||||
"rowIndex": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
noItemsMessage="No items found"
|
||||
responsive={true}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
</VisualizationContainer>
|
||||
`;
|
||||
|
||||
exports[`datatable_expression DatatableComponent it renders the title and value 1`] = `
|
||||
<VisualizationContainer
|
||||
reportTitle="My fanci metric chart"
|
||||
|
@ -32,6 +87,7 @@ exports[`datatable_expression DatatableComponent it renders the title and value
|
|||
"a": "shoes",
|
||||
"b": 1588024800000,
|
||||
"c": 3,
|
||||
"rowIndex": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -110,6 +110,24 @@ describe('datatable_expression', () => {
|
|||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders actions column when there are row actions', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
expect(
|
||||
shallow(
|
||||
<DatatableComponent
|
||||
data={data}
|
||||
args={args}
|
||||
formatFactory={(x) => x as IFieldFormat}
|
||||
onClickValue={onClickValue}
|
||||
getType={jest.fn()}
|
||||
onRowContextMenuClick={() => undefined}
|
||||
rowHasRowClickTriggerActions={[true, true, true]}
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it invokes executeTriggerActions with correct context on click on top value', () => {
|
||||
const { args, data } = sampleArgs();
|
||||
|
||||
|
|
|
@ -10,13 +10,22 @@ import React, { useMemo } from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiFlexGroup,
|
||||
EuiButtonIcon,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
EuiBasicTableColumn,
|
||||
EuiTableActionsColumnType,
|
||||
} from '@elastic/eui';
|
||||
import { IAggType } from 'src/plugins/data/public';
|
||||
import {
|
||||
FormatFactory,
|
||||
ILensInterpreterRenderHandlers,
|
||||
LensFilterEvent,
|
||||
LensMultiTable,
|
||||
LensTableRowContextMenuEvent,
|
||||
} from '../types';
|
||||
import {
|
||||
ExpressionFunctionDefinition,
|
||||
|
@ -45,7 +54,14 @@ export interface DatatableProps {
|
|||
type DatatableRenderProps = DatatableProps & {
|
||||
formatFactory: FormatFactory;
|
||||
onClickValue: (data: LensFilterEvent['data']) => void;
|
||||
onRowContextMenuClick?: (data: LensTableRowContextMenuEvent['data']) => void;
|
||||
getType: (name: string) => IAggType;
|
||||
|
||||
/**
|
||||
* A boolean for each table row, which is true if the row active
|
||||
* ROW_CLICK_TRIGGER actions attached to it, otherwise false.
|
||||
*/
|
||||
rowHasRowClickTriggerActions?: boolean[];
|
||||
};
|
||||
|
||||
export interface DatatableRender {
|
||||
|
@ -143,13 +159,47 @@ export const getDatatableRenderer = (dependencies: {
|
|||
const onClickValue = (data: LensFilterEvent['data']) => {
|
||||
handlers.event({ name: 'filter', data });
|
||||
};
|
||||
const onRowContextMenuClick = (data: LensTableRowContextMenuEvent['data']) => {
|
||||
handlers.event({ name: 'tableRowContextMenuClick', data });
|
||||
};
|
||||
const { hasCompatibleActions } = handlers;
|
||||
|
||||
// An entry for each table row, whether it has any actions attached to
|
||||
// ROW_CLICK_TRIGGER trigger.
|
||||
let rowHasRowClickTriggerActions: boolean[] = [];
|
||||
if (hasCompatibleActions) {
|
||||
const table = Object.values(config.data.tables)[0];
|
||||
if (!!table) {
|
||||
rowHasRowClickTriggerActions = await Promise.all(
|
||||
table.rows.map(async (row, rowIndex) => {
|
||||
try {
|
||||
const hasActions = await hasCompatibleActions({
|
||||
name: 'tableRowContextMenuClick',
|
||||
data: {
|
||||
rowIndex,
|
||||
table,
|
||||
columns: config.args.columns.columnIds,
|
||||
},
|
||||
});
|
||||
|
||||
return hasActions;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<DatatableComponent
|
||||
{...config}
|
||||
formatFactory={resolvedFormatFactory}
|
||||
onClickValue={onClickValue}
|
||||
onRowContextMenuClick={onRowContextMenuClick}
|
||||
getType={resolvedGetType}
|
||||
rowHasRowClickTriggerActions={rowHasRowClickTriggerActions}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
domNode,
|
||||
|
@ -169,7 +219,7 @@ export function DatatableComponent(props: DatatableRenderProps) {
|
|||
formatters[column.id] = props.formatFactory(column.meta?.params);
|
||||
});
|
||||
|
||||
const { onClickValue } = props;
|
||||
const { onClickValue, onRowContextMenuClick } = props;
|
||||
const handleFilterClick = useMemo(
|
||||
() => (field: string, value: unknown, colIndex: number, negate: boolean = false) => {
|
||||
const col = firstTable.columns[colIndex];
|
||||
|
@ -214,6 +264,124 @@ export function DatatableComponent(props: DatatableRenderProps) {
|
|||
return <EmptyPlaceholder icon={LensIconChartDatatable} />;
|
||||
}
|
||||
|
||||
const tableColumns: Array<
|
||||
EuiBasicTableColumn<{ rowIndex: number; [key: string]: unknown }>
|
||||
> = props.args.columns.columnIds
|
||||
.map((field) => {
|
||||
const col = firstTable.columns.find((c) => c.id === field);
|
||||
const filterable = bucketColumns.includes(field);
|
||||
const colIndex = firstTable.columns.findIndex((c) => c.id === field);
|
||||
return {
|
||||
field,
|
||||
name: (col && col.name) || '',
|
||||
render: (value: unknown) => {
|
||||
const formattedValue = formatters[field]?.convert(value);
|
||||
const fieldName = col?.meta?.field;
|
||||
|
||||
if (filterable) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className="lnsDataTable__cell"
|
||||
data-test-subj="lnsDataTableCellValueFilterable"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="none"
|
||||
alignItems="center"
|
||||
className="lnsDataTable__filter"
|
||||
>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('xpack.lens.includeValueButtonTooltip', {
|
||||
defaultMessage: 'Include value',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircle"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.lens.includeValueButtonAriaLabel', {
|
||||
defaultMessage: `Include {value}`,
|
||||
values: {
|
||||
value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`,
|
||||
},
|
||||
})}
|
||||
data-test-subj="lensDatatableFilterFor"
|
||||
onClick={() => handleFilterClick(field, value, colIndex)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('xpack.lens.excludeValueButtonTooltip', {
|
||||
defaultMessage: 'Exclude value',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="minusInCircle"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.lens.excludeValueButtonAriaLabel', {
|
||||
defaultMessage: `Exclude {value}`,
|
||||
values: {
|
||||
value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`,
|
||||
},
|
||||
})}
|
||||
data-test-subj="lensDatatableFilterOut"
|
||||
onClick={() => handleFilterClick(field, value, colIndex, true)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
return <span data-test-subj="lnsDataTableCellValue">{formattedValue}</span>;
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(({ field }) => !!field);
|
||||
|
||||
if (!!props.rowHasRowClickTriggerActions && !!onRowContextMenuClick) {
|
||||
const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions.find((x) => x);
|
||||
if (hasAtLeastOneRowClickAction) {
|
||||
const actions: EuiTableActionsColumnType<{ rowIndex: number; [key: string]: unknown }> = {
|
||||
name: i18n.translate('xpack.lens.datatable.actionsColumnName', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.tableRowMore', {
|
||||
defaultMessage: 'More',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.tableRowMoreDescription', {
|
||||
defaultMessage: 'Table row context menu',
|
||||
}),
|
||||
type: 'icon',
|
||||
icon: ({ rowIndex }: { rowIndex: number }) => {
|
||||
if (
|
||||
!!props.rowHasRowClickTriggerActions &&
|
||||
!props.rowHasRowClickTriggerActions[rowIndex]
|
||||
)
|
||||
return 'empty';
|
||||
return 'boxesVertical';
|
||||
},
|
||||
onClick: ({ rowIndex }) => {
|
||||
onRowContextMenuClick({
|
||||
rowIndex,
|
||||
table: firstTable,
|
||||
columns: props.args.columns.columnIds,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
tableColumns.push(actions);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VisualizationContainer
|
||||
reportTitle={props.args.title}
|
||||
|
@ -223,89 +391,8 @@ export function DatatableComponent(props: DatatableRenderProps) {
|
|||
className="lnsDataTable"
|
||||
data-test-subj="lnsDataTable"
|
||||
tableLayout="auto"
|
||||
columns={props.args.columns.columnIds
|
||||
.map((field) => {
|
||||
const col = firstTable.columns.find((c) => c.id === field);
|
||||
const filterable = bucketColumns.includes(field);
|
||||
const colIndex = firstTable.columns.findIndex((c) => c.id === field);
|
||||
return {
|
||||
field,
|
||||
name: (col && col.name) || '',
|
||||
render: (value: unknown) => {
|
||||
const formattedValue = formatters[field]?.convert(value);
|
||||
const fieldName = col?.meta?.field;
|
||||
|
||||
if (filterable) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className="lnsDataTable__cell"
|
||||
data-test-subj="lnsDataTableCellValueFilterable"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="none"
|
||||
alignItems="center"
|
||||
className="lnsDataTable__filter"
|
||||
>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('xpack.lens.includeValueButtonTooltip', {
|
||||
defaultMessage: 'Include value',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircle"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.lens.includeValueButtonAriaLabel', {
|
||||
defaultMessage: `Include {value}`,
|
||||
values: {
|
||||
value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`,
|
||||
},
|
||||
})}
|
||||
data-test-subj="lensDatatableFilterFor"
|
||||
onClick={() => handleFilterClick(field, value, colIndex)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('xpack.lens.excludeValueButtonTooltip', {
|
||||
defaultMessage: 'Exclude value',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="minusInCircle"
|
||||
color="text"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.excludeValueButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: `Exclude {value}`,
|
||||
values: {
|
||||
value: `${
|
||||
fieldName ? `${fieldName}: ` : ''
|
||||
}${formattedValue}`,
|
||||
},
|
||||
}
|
||||
)}
|
||||
data-test-subj="lensDatatableFilterOut"
|
||||
onClick={() => handleFilterClick(field, value, colIndex, true)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
return <span data-test-subj="lnsDataTableCellValue">{formattedValue}</span>;
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(({ field }) => !!field)}
|
||||
items={firstTable ? firstTable.rows : []}
|
||||
columns={tableColumns}
|
||||
items={firstTable ? firstTable.rows.map((row, rowIndex) => ({ ...row, rowIndex })) : []}
|
||||
/>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
|
|
|
@ -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(() =>
|
||||
|
|
|
@ -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: '' };
|
||||
|
|
|
@ -21,6 +21,8 @@ import { PaletteOutput } from 'src/plugins/charts/public';
|
|||
import { Subscription } from 'rxjs';
|
||||
import { toExpression, Ast } from '@kbn/interpreter/common';
|
||||
import { RenderMode } from 'src/plugins/expressions';
|
||||
import { map, distinctUntilChanged, skip } from 'rxjs/operators';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import {
|
||||
ExpressionRendererEvent,
|
||||
ReactExpressionRendererType,
|
||||
|
@ -38,7 +40,11 @@ import {
|
|||
import { Document, injectFilterReferences } from '../../persistence';
|
||||
import { ExpressionWrapper } from './expression_wrapper';
|
||||
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { isLensBrushEvent, isLensFilterEvent } from '../../types';
|
||||
import {
|
||||
isLensBrushEvent,
|
||||
isLensFilterEvent,
|
||||
isLensTableRowContextMenuClickEvent,
|
||||
} from '../../types';
|
||||
|
||||
import { IndexPatternsContract } from '../../../../../../src/plugins/data/public';
|
||||
import { getEditPath, DOC_TYPE } from '../../../common';
|
||||
|
@ -71,6 +77,7 @@ export interface LensEmbeddableDeps {
|
|||
timefilter: TimefilterContract;
|
||||
basePath: IBasePath;
|
||||
getTrigger?: UiActionsStart['getTrigger'] | undefined;
|
||||
getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions'];
|
||||
}
|
||||
|
||||
export class Embeddable
|
||||
|
@ -117,6 +124,36 @@ export class Embeddable
|
|||
this.autoRefreshFetchSubscription = deps.timefilter
|
||||
.getAutoRefreshFetch$()
|
||||
.subscribe(this.reload.bind(this));
|
||||
|
||||
const input$ = this.getInput$();
|
||||
|
||||
// Lens embeddable does not re-render when embeddable input changes in
|
||||
// general, to improve performance. This line makes sure the Lens embeddable
|
||||
// re-renders when anything in ".dynamicActions" (e.g. drilldowns) changes.
|
||||
input$
|
||||
.pipe(
|
||||
map((input) => input.enhancements?.dynamicActions),
|
||||
distinctUntilChanged((a, b) => isEqual(a, b)),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe((input) => {
|
||||
this.reload();
|
||||
});
|
||||
|
||||
// Lens embeddable does not re-render when embeddable input changes in
|
||||
// general, to improve performance. This line makes sure the Lens embeddable
|
||||
// re-renders when dashboard view mode switches between "view/edit". This is
|
||||
// needed to see the changes to ".dynamicActions" (e.g. drilldowns) when
|
||||
// dashboard's mode is toggled.
|
||||
input$
|
||||
.pipe(
|
||||
map((input) => input.viewMode),
|
||||
distinctUntilChanged(),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe((input) => {
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
||||
public supportedTriggers() {
|
||||
|
@ -127,6 +164,7 @@ export class Embeddable
|
|||
case 'lnsXY':
|
||||
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
|
||||
case 'lnsDatatable':
|
||||
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick];
|
||||
case 'lnsPie':
|
||||
return [VIS_EVENT_TO_TRIGGER.filter];
|
||||
case 'lnsMetric':
|
||||
|
@ -217,11 +255,31 @@ export class Embeddable
|
|||
handleEvent={this.handleEvent}
|
||||
onData$={this.updateActiveData}
|
||||
renderMode={input.renderMode}
|
||||
hasCompatibleActions={this.hasCompatibleActions}
|
||||
/>,
|
||||
domNode
|
||||
);
|
||||
}
|
||||
|
||||
private readonly hasCompatibleActions = async (
|
||||
event: ExpressionRendererEvent
|
||||
): Promise<boolean> => {
|
||||
if (isLensTableRowContextMenuClickEvent(event)) {
|
||||
const { getTriggerCompatibleActions } = this.deps;
|
||||
if (!getTriggerCompatibleActions) {
|
||||
return false;
|
||||
}
|
||||
const actions = await getTriggerCompatibleActions(VIS_EVENT_TO_TRIGGER[event.name], {
|
||||
data: event.data,
|
||||
embeddable: this,
|
||||
});
|
||||
|
||||
return actions.length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines the embeddable context with the saved object context, and replaces
|
||||
* any references to index patterns
|
||||
|
@ -264,6 +322,16 @@ export class Embeddable
|
|||
embeddable: this,
|
||||
});
|
||||
}
|
||||
|
||||
if (isLensTableRowContextMenuClickEvent(event)) {
|
||||
this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec(
|
||||
{
|
||||
data: event.data,
|
||||
embeddable: this,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async reload() {
|
||||
|
|
|
@ -94,6 +94,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
editable: await this.isEditable(),
|
||||
basePath: coreHttp.basePath,
|
||||
getTrigger: uiActions?.getTrigger,
|
||||
getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions,
|
||||
documentToExpression,
|
||||
},
|
||||
input,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui';
|
|||
import {
|
||||
ExpressionRendererEvent,
|
||||
ReactExpressionRendererType,
|
||||
ReactExpressionRendererProps,
|
||||
} from 'src/plugins/expressions/public';
|
||||
import { ExecutionContextSearch } from 'src/plugins/data/public';
|
||||
import { RenderMode } from 'src/plugins/expressions';
|
||||
|
@ -26,6 +27,7 @@ export interface ExpressionWrapperProps {
|
|||
handleEvent: (event: ExpressionRendererEvent) => void;
|
||||
onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void;
|
||||
renderMode?: RenderMode;
|
||||
hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions'];
|
||||
}
|
||||
|
||||
export function ExpressionWrapper({
|
||||
|
@ -37,6 +39,7 @@ export function ExpressionWrapper({
|
|||
searchSessionId,
|
||||
onData$,
|
||||
renderMode,
|
||||
hasCompatibleActions,
|
||||
}: ExpressionWrapperProps) {
|
||||
return (
|
||||
<I18nProvider>
|
||||
|
@ -80,6 +83,7 @@ export function ExpressionWrapper({
|
|||
</div>
|
||||
)}
|
||||
onEvent={handleEvent}
|
||||
hasCompatibleActions={hasCompatibleActions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
|
|||
import { CoreSetup } from 'kibana/public';
|
||||
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import { ROW_CLICK_TRIGGER } from '../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
ExpressionAstExpression,
|
||||
ExpressionRendererEvent,
|
||||
|
@ -614,11 +615,17 @@ export interface LensFilterEvent {
|
|||
name: 'filter';
|
||||
data: TriggerContext<typeof VALUE_CLICK_TRIGGER>['data'];
|
||||
}
|
||||
|
||||
export interface LensBrushEvent {
|
||||
name: 'brush';
|
||||
data: TriggerContext<typeof SELECT_RANGE_TRIGGER>['data'];
|
||||
}
|
||||
|
||||
export interface LensTableRowContextMenuEvent {
|
||||
name: 'tableRowContextMenuClick';
|
||||
data: TriggerContext<typeof ROW_CLICK_TRIGGER>['data'];
|
||||
}
|
||||
|
||||
export function isLensFilterEvent(event: ExpressionRendererEvent): event is LensFilterEvent {
|
||||
return event.name === 'filter';
|
||||
}
|
||||
|
@ -627,11 +634,17 @@ export function isLensBrushEvent(event: ExpressionRendererEvent): event is LensB
|
|||
return event.name === 'brush';
|
||||
}
|
||||
|
||||
export function isLensTableRowContextMenuClickEvent(
|
||||
event: ExpressionRendererEvent
|
||||
): event is LensBrushEvent {
|
||||
return event.name === 'tableRowContextMenuClick';
|
||||
}
|
||||
|
||||
/**
|
||||
* Expression renderer handlers specifically for lens renderers. This is a narrowed down
|
||||
* version of the general render handlers, specifying supported event types. If this type is
|
||||
* used, dispatched events will be handled correctly.
|
||||
*/
|
||||
export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandlers {
|
||||
event: (event: LensFilterEvent | LensBrushEvent) => void;
|
||||
event: (event: LensFilterEvent | LensBrushEvent | LensTableRowContextMenuEvent) => void;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { UrlDrilldownConfig, UrlDrilldownScope } from '../../../types';
|
||||
import { UrlDrilldownConfig } from '../../../types';
|
||||
import { UrlDrilldownCollectConfig } from '../url_drilldown_collect_config';
|
||||
|
||||
export const Demo = () => {
|
||||
|
@ -14,33 +14,13 @@ export const Demo = () => {
|
|||
url: { template: '' },
|
||||
});
|
||||
|
||||
const fakeScope: UrlDrilldownScope = {
|
||||
kibanaUrl: 'http://localhost:5601/',
|
||||
context: {
|
||||
filters: [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
{
|
||||
query: { match: { _type: { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
event: {
|
||||
key: 'fakeKey',
|
||||
value: 'fakeValue',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UrlDrilldownCollectConfig config={config} onConfig={onConfig} scope={fakeScope} />
|
||||
<UrlDrilldownCollectConfig
|
||||
config={config}
|
||||
onConfig={onConfig}
|
||||
variables={['event.key', 'event.value']}
|
||||
/>
|
||||
{JSON.stringify(config)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Demo } from './test_samples/demo';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
|
||||
htmlIdGenerator: () => () => `id-${Math.random()}`,
|
||||
}));
|
||||
|
||||
test('configure valid URL template', () => {
|
||||
const screen = render(<Demo />);
|
||||
|
||||
const urlTemplate = 'https://elastic.co/?{{event.key}}={{event.value}}';
|
||||
fireEvent.change(screen.getByLabelText(/Enter URL template/i), {
|
||||
target: { value: urlTemplate },
|
||||
});
|
||||
|
||||
const preview = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement;
|
||||
expect(preview.value).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`);
|
||||
expect(preview.disabled).toEqual(true);
|
||||
const previewLink = screen.getByText('Preview') as HTMLAnchorElement;
|
||||
expect(previewLink.href).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`);
|
||||
expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`);
|
||||
});
|
||||
|
||||
test('configure invalid URL template', () => {
|
||||
const screen = render(<Demo />);
|
||||
|
||||
const urlTemplate = 'https://elastic.co/?{{event.wrongKey}}={{event.wrongValue}}';
|
||||
fireEvent.change(screen.getByLabelText(/Enter URL template/i), {
|
||||
target: { value: urlTemplate },
|
||||
});
|
||||
|
||||
const previewTextArea = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement;
|
||||
expect(previewTextArea.disabled).toEqual(true);
|
||||
expect(previewTextArea.value).toEqual(urlTemplate);
|
||||
expect(screen.getByText(/invalid format/i)).toBeInTheDocument(); // check that error is shown
|
||||
|
||||
const previewLink = screen.getByText('Preview') as HTMLAnchorElement;
|
||||
expect(previewLink.href).toEqual(urlTemplate);
|
||||
expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`);
|
||||
});
|
|
@ -18,52 +18,40 @@ import {
|
|||
EuiTextArea,
|
||||
EuiSelectableOption,
|
||||
} from '@elastic/eui';
|
||||
import { UrlDrilldownConfig, UrlDrilldownScope } from '../../types';
|
||||
import { compile } from '../../url_template';
|
||||
import { validateUrlTemplate } from '../../url_validation';
|
||||
import { buildScopeSuggestions } from '../../url_drilldown_scope';
|
||||
import { UrlDrilldownConfig } from '../../types';
|
||||
import './index.scss';
|
||||
import {
|
||||
txtAddVariableButtonTitle,
|
||||
txtUrlPreviewHelpText,
|
||||
txtUrlTemplateSyntaxHelpLinkText,
|
||||
txtUrlTemplateVariablesHelpLinkText,
|
||||
txtUrlTemplateVariablesFilterPlaceholderText,
|
||||
txtUrlTemplateLabel,
|
||||
txtUrlTemplateOpenInNewTab,
|
||||
txtUrlTemplatePlaceholder,
|
||||
txtUrlTemplatePreviewLabel,
|
||||
txtUrlTemplatePreviewLinkText,
|
||||
} from './i18n';
|
||||
|
||||
export interface UrlDrilldownCollectConfig {
|
||||
config: UrlDrilldownConfig;
|
||||
variables: string[];
|
||||
onConfig: (newConfig: UrlDrilldownConfig) => void;
|
||||
scope: UrlDrilldownScope;
|
||||
syntaxHelpDocsLink?: string;
|
||||
variablesHelpDocsLink?: string;
|
||||
}
|
||||
|
||||
export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
|
||||
config,
|
||||
variables,
|
||||
onConfig,
|
||||
scope,
|
||||
syntaxHelpDocsLink,
|
||||
variablesHelpDocsLink,
|
||||
}) => {
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [showUrlError, setShowUrlError] = React.useState(false);
|
||||
const urlTemplate = config.url.template ?? '';
|
||||
const compiledUrl = React.useMemo(() => {
|
||||
try {
|
||||
return compile(urlTemplate, scope);
|
||||
} catch {
|
||||
return urlTemplate;
|
||||
}
|
||||
}, [urlTemplate, scope]);
|
||||
const scopeVariables = React.useMemo(() => buildScopeSuggestions(scope), [scope]);
|
||||
|
||||
function updateUrlTemplate(newUrlTemplate: string) {
|
||||
if (config.url.template !== newUrlTemplate) {
|
||||
setShowUrlError(true);
|
||||
onConfig({
|
||||
...config,
|
||||
url: {
|
||||
|
@ -73,18 +61,31 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
|
|||
});
|
||||
}
|
||||
}
|
||||
const { error, isValid } = React.useMemo(
|
||||
() => validateUrlTemplate({ template: urlTemplate }, scope),
|
||||
[urlTemplate, scope]
|
||||
);
|
||||
const isEmpty = !urlTemplate;
|
||||
const isInvalid = !isValid && !isEmpty;
|
||||
const isInvalid = showUrlError && isEmpty;
|
||||
const variablesDropdown = (
|
||||
<AddVariableButton
|
||||
variables={variables}
|
||||
variablesHelpLink={variablesHelpDocsLink}
|
||||
onSelect={(variable: string) => {
|
||||
if (textAreaRef.current) {
|
||||
updateUrlTemplate(
|
||||
urlTemplate.substr(0, textAreaRef.current!.selectionStart) +
|
||||
`{{${variable}}}` +
|
||||
urlTemplate.substr(textAreaRef.current!.selectionEnd)
|
||||
);
|
||||
} else {
|
||||
updateUrlTemplate(urlTemplate + `{{${variable}}}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={isInvalid}
|
||||
error={error}
|
||||
className={'uaeUrlDrilldownCollectConfig__urlTemplateFormRow'}
|
||||
label={txtUrlTemplateLabel}
|
||||
helpText={
|
||||
|
@ -94,23 +95,7 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
|
|||
</EuiLink>
|
||||
)
|
||||
}
|
||||
labelAppend={
|
||||
<AddVariableButton
|
||||
variables={scopeVariables}
|
||||
variablesHelpLink={variablesHelpDocsLink}
|
||||
onSelect={(variable: string) => {
|
||||
if (textAreaRef.current) {
|
||||
updateUrlTemplate(
|
||||
urlTemplate.substr(0, textAreaRef.current!.selectionStart) +
|
||||
`{{${variable}}}` +
|
||||
urlTemplate.substr(textAreaRef.current!.selectionEnd)
|
||||
);
|
||||
} else {
|
||||
updateUrlTemplate(urlTemplate + `{{${variable}}}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
labelAppend={variablesDropdown}
|
||||
>
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
|
@ -120,31 +105,11 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
|
|||
value={urlTemplate}
|
||||
placeholder={txtUrlTemplatePlaceholder}
|
||||
onChange={(event) => updateUrlTemplate(event.target.value)}
|
||||
onBlur={() => setShowUrlError(true)}
|
||||
rows={3}
|
||||
inputRef={textAreaRef}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={txtUrlTemplatePreviewLabel}
|
||||
labelAppend={
|
||||
<EuiText size="xs">
|
||||
<EuiLink href={compiledUrl} target="_blank" external>
|
||||
{txtUrlTemplatePreviewLinkText}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
}
|
||||
helpText={txtUrlPreviewHelpText}
|
||||
>
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
name="urlPreview"
|
||||
data-test-subj="urlPreview"
|
||||
value={compiledUrl}
|
||||
disabled={true}
|
||||
rows={3}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow hasChildLabel={false}>
|
||||
<EuiCheckbox
|
||||
id="openInNewTab"
|
||||
|
|
|
@ -12,7 +12,3 @@ export {
|
|||
} from './url_validation';
|
||||
export { compile as urlDrilldownCompileUrl } from './url_template';
|
||||
export { globalScopeProvider as urlDrilldownGlobalScopeProvider } from './url_drilldown_global_scope';
|
||||
export {
|
||||
buildScope as urlDrilldownBuildScope,
|
||||
buildScopeSuggestions as urlDrilldownBuildScopeSuggestions,
|
||||
} from './url_drilldown_scope';
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { buildScope, buildScopeSuggestions } from './url_drilldown_scope';
|
||||
|
||||
test('buildScopeSuggestions', () => {
|
||||
expect(
|
||||
buildScopeSuggestions(
|
||||
buildScope({
|
||||
globalScope: {
|
||||
kibanaUrl: 'http://localhost:5061/',
|
||||
},
|
||||
eventScope: {
|
||||
key: '__testKey__',
|
||||
value: '__testValue__',
|
||||
},
|
||||
contextScope: {
|
||||
filters: [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
{
|
||||
query: { match: { _type: { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
],
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kquery',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"event.key",
|
||||
"event.value",
|
||||
"context.filters",
|
||||
"context.query.language",
|
||||
"context.query.query",
|
||||
"kibanaUrl",
|
||||
]
|
||||
`);
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { partition } from 'lodash';
|
||||
import { getFlattenedObject } from '@kbn/std';
|
||||
import { UrlDrilldownGlobalScope, UrlDrilldownScope } from './types';
|
||||
|
||||
export function buildScope<
|
||||
ContextScope extends object = object,
|
||||
EventScope extends object = object
|
||||
>({
|
||||
globalScope,
|
||||
contextScope,
|
||||
eventScope,
|
||||
}: {
|
||||
globalScope: UrlDrilldownGlobalScope;
|
||||
contextScope?: ContextScope;
|
||||
eventScope?: EventScope;
|
||||
}): UrlDrilldownScope<ContextScope, EventScope> {
|
||||
return {
|
||||
...globalScope,
|
||||
context: contextScope,
|
||||
event: eventScope,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds list of variables for suggestion from scope
|
||||
* keys sorted alphabetically, except {{event.$}} variables are pulled to the top
|
||||
* @param scope
|
||||
*/
|
||||
export function buildScopeSuggestions(scope: UrlDrilldownGlobalScope): string[] {
|
||||
const allKeys = Object.keys(getFlattenedObject(scope)).sort();
|
||||
const [eventKeys, otherKeys] = partition(allKeys, (key) => key.startsWith('event'));
|
||||
return [...eventKeys, ...otherKeys];
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue