From f52c5c10c43078ce9656e1b477046d25e806d639 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 14 Jul 2020 17:41:20 +0200 Subject: [PATCH 01/12] pass meta everywhere --- .../public/actions/actions.tsx | 3 +- .../ui_actions/public/actions/action.ts | 33 ++++++++++++++++--- .../public/actions/action_internal.ts | 16 ++++----- .../build_eui_context_menu_panels.tsx | 17 +++++++--- src/plugins/ui_actions/public/index.ts | 2 +- .../public/service/ui_actions_service.ts | 4 ++- .../public/triggers/trigger_internal.ts | 3 +- .../ui_actions/public/util/presentable.ts | 14 ++++---- .../dashboard_to_url_drilldown/index.tsx | 8 ++++- .../public/drilldowns/drilldown_definition.ts | 7 ++-- .../public/dynamic_actions/action_factory.ts | 4 +-- .../dynamic_actions/dynamic_action_manager.ts | 4 +-- .../ui_actions_service_enhancements.ts | 6 ++-- 13 files changed, 85 insertions(+), 36 deletions(-) diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index 4ef8d5bf4d9c66..fdf414f7368ff8 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -37,7 +37,8 @@ export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY'; export const showcasePluggability = createAction({ type: ACTION_SHOWCASE_PLUGGABILITY, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', - execute: async () => alert("Isn't that cool?!"), + execute: async (context, { trigger }) => + alert(`Isn't that cool?! Triggered by ${trigger?.id} trigger`), }); export interface PhoneContext { diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index f5dbbc9f923acd..e303823459792b 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -20,11 +20,23 @@ import { UiComponent } from 'src/plugins/kibana_utils/public'; import { ActionType, ActionContextMapping } from '../types'; import { Presentable } from '../util/presentable'; +import { Trigger } from '../triggers'; export type ActionByType = Action; +/** + * Metadata passed into execution handlers + */ +export interface EventMeta { + /** + * Trigger that executed the action + * Could be empty for cases when actions executed directly with (no trigger) + */ + trigger?: Trigger; +} + export interface Action - extends Partial> { + extends Partial> { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. @@ -62,12 +74,12 @@ export interface Action * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. */ - isCompatible(context: Context): Promise; + isCompatible(context: Context, meta?: EventMeta): Promise; /** * Executes the action. */ - execute(context: Context): Promise; + execute(context: Context, meta?: EventMeta): Promise; } /** @@ -85,10 +97,23 @@ export interface ActionDefinition */ readonly type?: ActionType; + /** + * Returns a promise that resolves to true if this item is compatible given + * the context and should be displayed to user, otherwise resolves to false. + */ + isCompatible?(context: Context, meta: EventMeta): Promise; + /** * Executes the action. */ - execute(context: Context): Promise; + execute(context: Context, meta: EventMeta): Promise; + + /** + * This method should return a link if this item can be clicked on. The link + * is used to navigate user if user middle-clicks it or Ctrl + clicks or + * right-clicks and selects "Open in new tab". + */ + getHref?(context: Context, meta: EventMeta): Promise; } export type ActionContext = A extends ActionDefinition ? Context : never; diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index 10eb760b130898..f46572c90150b9 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -19,7 +19,7 @@ // @ts-ignore import React from 'react'; -import { Action, ActionContext as Context, ActionDefinition } from './action'; +import { Action, ActionContext as Context, ActionDefinition, EventMeta } from './action'; import { Presentable } from '../util/presentable'; import { uiToReactComponent } from '../../../kibana_react/public'; import { ActionType } from '../types'; @@ -28,7 +28,7 @@ import { ActionType } from '../types'; * @internal */ export class ActionInternal - implements Action>, Presentable> { + implements Action>, Presentable, EventMeta> { constructor(public readonly definition: A) {} public readonly id: string = this.definition.id; @@ -37,8 +37,8 @@ export class ActionInternal public readonly MenuItem? = this.definition.MenuItem; public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; - public execute(context: Context) { - return this.definition.execute(context); + public execute(context: Context, meta: EventMeta = {}) { + return this.definition.execute(context, meta); } public getIconType(context: Context): string | undefined { @@ -56,13 +56,13 @@ export class ActionInternal return this.definition.getDisplayNameTooltip(context); } - public async isCompatible(context: Context): Promise { + public async isCompatible(context: Context, meta: EventMeta = {}): Promise { if (!this.definition.isCompatible) return true; - return await this.definition.isCompatible(context); + return await this.definition.isCompatible(context, meta); } - public async getHref(context: Context): Promise { + public async getHref(context: Context, meta: EventMeta = {}): Promise { if (!this.definition.getHref) return undefined; - return await this.definition.getHref(context); + return await this.definition.getHref(context, meta); } } diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 74e9ef96b575b9..b1eb2d436edfd5 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -23,6 +23,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { uiToReactComponent } from '../../../kibana_react/public'; import { Action } from '../actions'; +import { Trigger } from '../triggers'; export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { defaultMessage: 'Options', @@ -34,17 +35,20 @@ export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { export async function buildContextMenuForActions({ actions, actionContext, + trigger, title = defaultTitle, closeMenu, }: { actions: Array>; actionContext: Context; + trigger?: Trigger; title?: string; closeMenu: () => void; }): Promise { const menuItems = await buildEuiContextMenuPanelItems({ actions, actionContext, + trigger, closeMenu, }); @@ -61,15 +65,17 @@ export async function buildContextMenuForActions({ async function buildEuiContextMenuPanelItems({ actions, actionContext, + trigger, closeMenu, }: { actions: Array>; actionContext: Context; + trigger?: Trigger; closeMenu: () => void; }) { const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length); const promises = actions.map(async (action, index) => { - const isCompatible = await action.isCompatible(actionContext); + const isCompatible = await action.isCompatible(actionContext, { trigger }); if (!isCompatible) { return; } @@ -77,6 +83,7 @@ async function buildEuiContextMenuPanelItems({ items[index] = await convertPanelActionToContextMenuItem({ action, actionContext, + trigger, closeMenu, }); }); @@ -89,10 +96,12 @@ async function buildEuiContextMenuPanelItems({ async function convertPanelActionToContextMenuItem({ action, actionContext, + trigger, closeMenu, }: { action: Action; actionContext: Context; + trigger?: Trigger; closeMenu: () => void; }): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { @@ -116,20 +125,20 @@ async function convertPanelActionToContextMenuItem({ !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys ) { event.preventDefault(); - action.execute(actionContext); + action.execute(actionContext, { trigger }); } else { // let browser handle navigation } } else { // not a link - action.execute(actionContext); + action.execute(actionContext, { trigger }); } closeMenu(); }; if (action.getHref) { - const href = await action.getHref(actionContext); + const href = await action.getHref(actionContext, { trigger }); if (href) { menuPanelItem.href = href; } diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index a9b413fb36542d..be867f2b0564f5 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -45,4 +45,4 @@ export { applyFilterTrigger, } from './triggers'; export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types'; -export { ActionByType } from './actions'; +export { ActionByType, EventMeta } from './actions'; diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 11f5769a946483..57e8239730d32e 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -176,7 +176,9 @@ export class UiActionsService { context: TriggerContextMapping[T] ): Promise>> => { const actions = this.getTriggerActions!(triggerId); - const isCompatibles = await Promise.all(actions.map((action) => action.isCompatible(context))); + const isCompatibles = await Promise.all( + actions.map((action) => action.isCompatible(context, { trigger: this.getTrigger(triggerId) })) + ); return actions.reduce( (acc: Array>, action, i) => isCompatibles[i] ? [...acc, action] : acc, diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index e499c404ae7457..c9a02da4b2700e 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -55,7 +55,7 @@ export class TriggerInternal { action: Action, context: TriggerContextMapping[T] ) { - await action.execute(context); + await action.execute(context, { trigger: this.trigger }); } private async executeMultipleActions( @@ -67,6 +67,7 @@ export class TriggerInternal { actionContext: context, title: this.trigger.title, closeMenu: () => session.close(), + trigger: this.trigger, }); const session = openContextMenu([panel], { 'data-test-subj': 'multipleActionsContextMenu', diff --git a/src/plugins/ui_actions/public/util/presentable.ts b/src/plugins/ui_actions/public/util/presentable.ts index 57070f7673f61c..2cc382c65507ab 100644 --- a/src/plugins/ui_actions/public/util/presentable.ts +++ b/src/plugins/ui_actions/public/util/presentable.ts @@ -22,7 +22,7 @@ import { UiComponent } from 'src/plugins/kibana_utils/public'; /** * Represents something that can be displayed to user in UI. */ -export interface Presentable { +export interface Presentable { /** * ID that uniquely identifies this object. */ @@ -38,34 +38,34 @@ export interface Presentable { * `UiComponent` to render when displaying this entity as a context menu item. * If not provided, `getDisplayName` will be used instead. */ - readonly MenuItem?: UiComponent<{ context: Context }>; + readonly MenuItem?: UiComponent<{ context: Context; meta?: Meta }>; /** * Optional EUI icon type that can be displayed along with the title. */ - getIconType(context: Context): string | undefined; + getIconType(context: Context, meta?: Meta): string | undefined; /** * Returns a title to be displayed to the user. */ - getDisplayName(context: Context): string; + getDisplayName(context: Context, meta?: Meta): string; /** * Returns tooltip text which should be displayed when user hovers this object. * Should return empty string if tooltip should not be displayed. */ - getDisplayNameTooltip(context: Context): string; + getDisplayNameTooltip(context: Context, meta?: Meta): string; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: Context): Promise; + getHref?(context: Context, meta?: Meta): Promise; /** * Returns a promise that resolves to true if this item is compatible given * the context and should be displayed to user, otherwise resolves to false. */ - isCompatible(context: Context): Promise; + isCompatible(context: Context, meta?: Meta): Promise; } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 037e017097e533..07bc61b2b1b6c0 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -10,6 +10,7 @@ import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/publ import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; +import { EventMeta } from '../../../../../src/plugins/ui_actions/public/actions'; function isValidUrl(url: string) { try { @@ -101,7 +102,12 @@ export class DashboardToUrlDrilldown implements Drilldown return config.url; }; - public readonly execute = async (config: Config, context: ActionContext) => { + public readonly execute = async (config: Config, context: ActionContext, event: EventMeta) => { + // Just for showcasing: + // we can get trigger a which caused this drilldown execution + // eslint-disable-next-line no-console + console.log(event.trigger?.id); + const url = await this.getHref(config, context); if (config.openInNewTab) { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index a41ae851e185b0..ea47edf599d633 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -6,6 +6,7 @@ import { ActionFactoryDefinition } from '../dynamic_actions'; import { LicenseType } from '../../../licensing/public'; +import { EventMeta } from '../../../../../src/plugins/ui_actions/public/'; /** * This is a convenience interface to register a drilldown. Drilldown has @@ -92,11 +93,13 @@ export interface DrilldownDefinition< * @param config Config object that user configured this drilldown with. * @param context Object that represents context in which the underlying * `UIAction` of this drilldown is being executed in. + * @param meta object with meta information about underlying `UiAction` execution. + * For example, contains `trigger` which fired corresponding `UiAction` */ - execute(config: Config, context: ExecutionContext): void; + execute(config: Config, context: ExecutionContext, meta: EventMeta): void; /** * A link where drilldown should navigate on middle click or Ctrl + click. */ - getHref?(config: Config, context: ExecutionContext): Promise; + getHref?(config: Config, context: ExecutionContext, meta: EventMeta): Promise; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 95b7941b48ed35..60bad4ae90b471 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -67,10 +67,10 @@ export class ActionFactory< const action = this.def.create(serializedAction); return { ...action, - isCompatible: async (context: ActionContext): Promise => { + isCompatible: async (context: ActionContext, meta): Promise => { if (!this.isCompatibleLicence()) return false; if (!action.isCompatible) return true; - return action.isCompatible(context); + return action.isCompatible(context, meta); }, }; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 4afefe3006a43c..5d7fcacc9a6406 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -78,10 +78,10 @@ export class DynamicActionManager { uiActions.registerAction({ ...actionDefinition, id: actionId, - isCompatible: async (context) => { + isCompatible: async (context, meta) => { if (!(await isCompatible(context))) return false; if (!actionDefinition.isCompatible) return true; - return actionDefinition.isCompatible(context); + return actionDefinition.isCompatible(context, meta); }, }); for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId); diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index bd05659d59e9d8..e195d1ce36df1c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -96,8 +96,10 @@ export class UiActionsServiceEnhancements { type: factoryId, getIconType: () => euiIcon, getDisplayName: () => serializedAction.name, - execute: async (context) => await execute(serializedAction.config, context), - getHref: getHref ? async (context) => getHref(serializedAction.config, context) : undefined, + execute: async (context, meta) => await execute(serializedAction.config, context, meta), + getHref: getHref + ? async (context, meta) => getHref(serializedAction.config, context, meta) + : undefined, }), } as ActionFactoryDefinition; From 0f109196ce92cb2c956215e059a6322fc7cef5ed Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 15 Jul 2020 13:15:08 +0200 Subject: [PATCH 02/12] simplify --- .../public/actions/actions.tsx | 4 +- .../ui_actions/public/actions/action.ts | 39 ++++++++++++------- .../public/actions/action_internal.ts | 16 ++++---- .../build_eui_context_menu_panels.tsx | 20 ++++++++-- .../public/service/ui_actions_service.ts | 7 +++- .../public/triggers/trigger_internal.ts | 5 ++- src/plugins/ui_actions/public/types.ts | 14 ++++++- .../ui_actions/public/util/presentable.ts | 14 +++---- 8 files changed, 81 insertions(+), 38 deletions(-) diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index fdf414f7368ff8..afacb13a1c734d 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -37,8 +37,8 @@ export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY'; export const showcasePluggability = createAction({ type: ACTION_SHOWCASE_PLUGGABILITY, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', - execute: async (context, { trigger }) => - alert(`Isn't that cool?! Triggered by ${trigger?.id} trigger`), + execute: async (context) => + alert(`Isn't that cool?! Triggered by ${context.trigger?.id} trigger`), }); export interface PhoneContext { diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index e303823459792b..3d9b664c886fcf 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -18,25 +18,29 @@ */ import { UiComponent } from 'src/plugins/kibana_utils/public'; -import { ActionType, ActionContextMapping } from '../types'; +import { ActionType, ActionContextMapping, BaseContext } from '../types'; import { Presentable } from '../util/presentable'; import { Trigger } from '../triggers'; export type ActionByType = Action; /** - * Metadata passed into execution handlers + * During action execution we can provide additional information, + * for example, trigger, that caused the action execution */ -export interface EventMeta { +export interface ExecutionMeta { /** * Trigger that executed the action - * Could be empty for cases when actions executed directly with (no trigger) + * Optional, since action could be run without a trigger */ trigger?: Trigger; } -export interface Action - extends Partial> { +export interface Action< + Context extends BaseContext = {}, + T = ActionType, + ExecutionContext extends Context & ExecutionMeta = Context & ExecutionMeta +> extends Partial> { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. @@ -74,19 +78,28 @@ export interface Action * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. */ - isCompatible(context: Context, meta?: EventMeta): Promise; + isCompatible(context: ExecutionContext): Promise; /** * Executes the action. */ - execute(context: Context, meta?: EventMeta): Promise; + execute(context: ExecutionContext): Promise; + + /** + * This method should return a link if this item can be clicked on. The link + * is used to navigate user if user middle-clicks it or Ctrl + clicks or + * right-clicks and selects "Open in new tab". + */ + getHref?(context: ExecutionContext): Promise; } /** * A convenience interface used to register an action. */ -export interface ActionDefinition - extends Partial> { +export interface ActionDefinition< + Context extends BaseContext = {}, + ExecutionContext extends Context & ExecutionMeta = Context & ExecutionMeta +> extends Partial> { /** * ID of the action that uniquely identifies this action in the actions registry. */ @@ -101,19 +114,19 @@ export interface ActionDefinition * Returns a promise that resolves to true if this item is compatible given * the context and should be displayed to user, otherwise resolves to false. */ - isCompatible?(context: Context, meta: EventMeta): Promise; + isCompatible?(context: ExecutionContext): Promise; /** * Executes the action. */ - execute(context: Context, meta: EventMeta): Promise; + execute(context: ExecutionContext): Promise; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: Context, meta: EventMeta): Promise; + getHref?(context: ExecutionContext): Promise; } export type ActionContext = A extends ActionDefinition ? Context : never; diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index f46572c90150b9..10eb760b130898 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -19,7 +19,7 @@ // @ts-ignore import React from 'react'; -import { Action, ActionContext as Context, ActionDefinition, EventMeta } from './action'; +import { Action, ActionContext as Context, ActionDefinition } from './action'; import { Presentable } from '../util/presentable'; import { uiToReactComponent } from '../../../kibana_react/public'; import { ActionType } from '../types'; @@ -28,7 +28,7 @@ import { ActionType } from '../types'; * @internal */ export class ActionInternal - implements Action>, Presentable, EventMeta> { + implements Action>, Presentable> { constructor(public readonly definition: A) {} public readonly id: string = this.definition.id; @@ -37,8 +37,8 @@ export class ActionInternal public readonly MenuItem? = this.definition.MenuItem; public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; - public execute(context: Context, meta: EventMeta = {}) { - return this.definition.execute(context, meta); + public execute(context: Context) { + return this.definition.execute(context); } public getIconType(context: Context): string | undefined { @@ -56,13 +56,13 @@ export class ActionInternal return this.definition.getDisplayNameTooltip(context); } - public async isCompatible(context: Context, meta: EventMeta = {}): Promise { + public async isCompatible(context: Context): Promise { if (!this.definition.isCompatible) return true; - return await this.definition.isCompatible(context, meta); + return await this.definition.isCompatible(context); } - public async getHref(context: Context, meta: EventMeta = {}): Promise { + public async getHref(context: Context): Promise { if (!this.definition.getHref) return undefined; - return await this.definition.getHref(context, meta); + return await this.definition.getHref(context); } } diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index b1eb2d436edfd5..4987602ae1cd57 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -75,7 +75,10 @@ async function buildEuiContextMenuPanelItems({ }) { const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length); const promises = actions.map(async (action, index) => { - const isCompatible = await action.isCompatible(actionContext, { trigger }); + const isCompatible = await action.isCompatible({ + ...actionContext, + trigger, + }); if (!isCompatible) { return; } @@ -125,20 +128,29 @@ async function convertPanelActionToContextMenuItem({ !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys ) { event.preventDefault(); - action.execute(actionContext, { trigger }); + action.execute({ + ...actionContext, + trigger, + }); } else { // let browser handle navigation } } else { // not a link - action.execute(actionContext, { trigger }); + action.execute({ + ...actionContext, + trigger, + }); } closeMenu(); }; if (action.getHref) { - const href = await action.getHref(actionContext, { trigger }); + const href = await action.getHref({ + ...actionContext, + trigger, + }); if (href) { menuPanelItem.href = href; } diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 57e8239730d32e..b29f51e44afba4 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -177,7 +177,12 @@ export class UiActionsService { ): Promise>> => { const actions = this.getTriggerActions!(triggerId); const isCompatibles = await Promise.all( - actions.map((action) => action.isCompatible(context, { trigger: this.getTrigger(triggerId) })) + actions.map((action) => + action.isCompatible({ + ...context, + trigger: this.getTrigger(triggerId), + }) + ) ); return actions.reduce( (acc: Array>, action, i) => diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index c9a02da4b2700e..5901bc4cfb9980 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -55,7 +55,10 @@ export class TriggerInternal { action: Action, context: TriggerContextMapping[T] ) { - await action.execute(context, { trigger: this.trigger }); + await action.execute({ + ...context, + trigger: this.trigger, + }); } private async executeMultipleActions( diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 9fcd8a32881df3..49c8d08daa179d 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -20,7 +20,12 @@ import { ActionInternal } from './actions/action_internal'; import { TriggerInternal } from './triggers/trigger_internal'; import { Filter } from '../../data/public'; -import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + APPLY_FILTER_TRIGGER, + Trigger, +} from './triggers'; import { IEmbeddable } from '../../embeddable/public'; import { RangeSelectContext, ValueClickContext } from '../../embeddable/public'; @@ -34,6 +39,11 @@ export type TriggerId = keyof TriggerContextMapping; export type BaseContext = object; export type TriggerContext = BaseContext; +export interface BaseActionContext { + meta?: { + trigger?: Trigger; + }; +} export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; @@ -49,5 +59,5 @@ const DEFAULT_ACTION = ''; export type ActionType = keyof ActionContextMapping; export interface ActionContextMapping { - [DEFAULT_ACTION]: BaseContext; + [DEFAULT_ACTION]: BaseActionContext; } diff --git a/src/plugins/ui_actions/public/util/presentable.ts b/src/plugins/ui_actions/public/util/presentable.ts index 2cc382c65507ab..57070f7673f61c 100644 --- a/src/plugins/ui_actions/public/util/presentable.ts +++ b/src/plugins/ui_actions/public/util/presentable.ts @@ -22,7 +22,7 @@ import { UiComponent } from 'src/plugins/kibana_utils/public'; /** * Represents something that can be displayed to user in UI. */ -export interface Presentable { +export interface Presentable { /** * ID that uniquely identifies this object. */ @@ -38,34 +38,34 @@ export interface Presentable; + readonly MenuItem?: UiComponent<{ context: Context }>; /** * Optional EUI icon type that can be displayed along with the title. */ - getIconType(context: Context, meta?: Meta): string | undefined; + getIconType(context: Context): string | undefined; /** * Returns a title to be displayed to the user. */ - getDisplayName(context: Context, meta?: Meta): string; + getDisplayName(context: Context): string; /** * Returns tooltip text which should be displayed when user hovers this object. * Should return empty string if tooltip should not be displayed. */ - getDisplayNameTooltip(context: Context, meta?: Meta): string; + getDisplayNameTooltip(context: Context): string; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: Context, meta?: Meta): Promise; + getHref?(context: Context): Promise; /** * Returns a promise that resolves to true if this item is compatible given * the context and should be displayed to user, otherwise resolves to false. */ - isCompatible(context: Context, meta?: Meta): Promise; + isCompatible(context: Context): Promise; } From 55de45630f9ab091b3e401e2b68d9c3a99c11174 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 15 Jul 2020 14:00:46 +0200 Subject: [PATCH 03/12] wip --- .../actions/clone_panel_action.tsx | 10 ++++-- .../actions/expand_panel_action.tsx | 10 ++++-- .../actions/replace_panel_action.tsx | 10 ++++-- .../ui_actions/public/actions/action.test.ts | 4 ++- .../ui_actions/public/actions/action.ts | 31 +++++++++---------- src/plugins/ui_actions/public/index.ts | 2 +- 6 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index 26af13b4410fe6..ab2fa47d9d04c9 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -21,7 +21,11 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from 'src/core/public'; import uuid from 'uuid'; import _ from 'lodash'; -import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; +import { + ActionByType, + ActionExecutionContext, + IncompatibleActionError, +} from '../../ui_actions_plugin'; import { ViewMode, PanelState, IEmbeddable } from '../../embeddable_plugin'; import { SavedObject } from '../../../../saved_objects/public'; import { PanelNotFoundError, EmbeddableInput } from '../../../../embeddable/public'; @@ -60,7 +64,7 @@ export class ClonePanelAction implements ActionByType return 'copy'; } - public async isCompatible({ embeddable }: ClonePanelActionContext) { + public async isCompatible({ embeddable }: ActionExecutionContext) { return Boolean( embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && @@ -69,7 +73,7 @@ export class ClonePanelAction implements ActionByType ); } - public async execute({ embeddable }: ClonePanelActionContext) { + public async execute({ embeddable }: ActionExecutionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { throw new IncompatibleActionError(); } diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx index d0442fbc26073d..0918ee8a7db7f7 100644 --- a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx @@ -19,7 +19,11 @@ import { i18n } from '@kbn/i18n'; import { IEmbeddable } from '../../embeddable_plugin'; -import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; +import { + ActionByType, + ActionExecutionContext, + IncompatibleActionError, +} from '../../ui_actions_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; export const ACTION_EXPAND_PANEL = 'togglePanel'; @@ -69,11 +73,11 @@ export class ExpandPanelAction implements ActionByType) { return Boolean(embeddable.parent && isDashboard(embeddable.parent)); } - public async execute({ embeddable }: ExpandPanelActionContext) { + public async execute({ embeddable }: ActionExecutionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx index 5526af2f83850c..1b96228efad419 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx @@ -21,7 +21,11 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from 'src/core/public'; import { IEmbeddable, ViewMode, EmbeddableStart } from '../../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; -import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; +import { + ActionByType, + ActionExecutionContext, + IncompatibleActionError, +} from '../../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; export const ACTION_REPLACE_PANEL = 'replacePanel'; @@ -62,7 +66,7 @@ export class ReplacePanelAction implements ActionByType) { if (embeddable.getInput().viewMode) { if (embeddable.getInput().viewMode === ViewMode.VIEW) { return false; @@ -72,7 +76,7 @@ export class ReplacePanelAction implements ActionByType) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } diff --git a/src/plugins/ui_actions/public/actions/action.test.ts b/src/plugins/ui_actions/public/actions/action.test.ts index f9d696d3ddb5f4..38ebd4fe61dfef 100644 --- a/src/plugins/ui_actions/public/actions/action.test.ts +++ b/src/plugins/ui_actions/public/actions/action.test.ts @@ -19,12 +19,14 @@ import { createAction } from '../../../ui_actions/public'; import { ActionType } from '../types'; +import { ActionExecutionContext } from './action'; const sayHelloAction = createAction({ // Casting to ActionType is a hack - in a real situation use // declare module and add this id to ActionContextMapping. type: 'test' as ActionType, - isCompatible: ({ amICompatible }: { amICompatible: boolean }) => Promise.resolve(amICompatible), + isCompatible: ({ amICompatible }: ActionExecutionContext<{ amICompatible: boolean }>) => + Promise.resolve(amICompatible), execute: () => Promise.resolve(), }); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 3d9b664c886fcf..25bd6cceaf310a 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -28,19 +28,18 @@ export type ActionByType = Action extends Partial> { +export type ActionExecutionContext = Context & ActionExecutionMeta; + +export interface Action + extends Partial> { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. @@ -78,28 +77,26 @@ export interface Action< * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. */ - isCompatible(context: ExecutionContext): Promise; + isCompatible(context: ActionExecutionContext): Promise; /** * Executes the action. */ - execute(context: ExecutionContext): Promise; + execute(context: ActionExecutionContext): Promise; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: ExecutionContext): Promise; + getHref?(context: ActionExecutionContext): Promise; } /** * A convenience interface used to register an action. */ -export interface ActionDefinition< - Context extends BaseContext = {}, - ExecutionContext extends Context & ExecutionMeta = Context & ExecutionMeta -> extends Partial> { +export interface ActionDefinition + extends Partial> { /** * ID of the action that uniquely identifies this action in the actions registry. */ @@ -114,19 +111,19 @@ export interface ActionDefinition< * Returns a promise that resolves to true if this item is compatible given * the context and should be displayed to user, otherwise resolves to false. */ - isCompatible?(context: ExecutionContext): Promise; + isCompatible?(context: ActionExecutionContext): Promise; /** * Executes the action. */ - execute(context: ExecutionContext): Promise; + execute(context: ActionExecutionContext): Promise; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: ExecutionContext): Promise; + getHref?(context: ActionExecutionContext): Promise; } export type ActionContext = A extends ActionDefinition ? Context : never; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index be867f2b0564f5..34b6fc3ba0771e 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -45,4 +45,4 @@ export { applyFilterTrigger, } from './triggers'; export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types'; -export { ActionByType, EventMeta } from './actions'; +export { ActionByType, ActionExecutionContext, ActionExecutionMeta } from './actions'; From 865cc0626301dc3983cefdc10c045d5acf7d50f9 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 15 Jul 2020 14:01:01 +0200 Subject: [PATCH 04/12] test --- src/plugins/ui_actions/public/actions/action.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 25bd6cceaf310a..0c65a4201d14a2 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -36,7 +36,8 @@ export interface ActionExecutionMeta { trigger?: Trigger; } -export type ActionExecutionContext = Context & ActionExecutionMeta; +export type ActionExecutionContext = Context & + ActionExecutionMeta; export interface Action extends Partial> { From 2407d492da3f3b0dc946c30bccb1c39bb706634e Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 15 Jul 2020 14:51:11 +0200 Subject: [PATCH 05/12] simpler --- .../ui_actions_explorer/public/actions/actions.tsx | 8 ++++++-- .../application/actions/clone_panel_action.tsx | 10 +++------- .../application/actions/expand_panel_action.tsx | 10 +++------- .../application/actions/replace_panel_action.tsx | 10 +++------- src/plugins/ui_actions/public/actions/action.ts | 14 +++++++------- src/plugins/ui_actions/public/types.ts | 14 ++------------ .../public/dashboard_to_url_drilldown/index.tsx | 9 ++++++--- .../public/drilldowns/drilldown_definition.ts | 14 +++++++++----- .../public/dynamic_actions/action_factory.ts | 4 ++-- .../dynamic_actions/dynamic_action_manager.ts | 4 ++-- .../services/ui_actions_service_enhancements.ts | 6 ++---- 11 files changed, 45 insertions(+), 58 deletions(-) diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index afacb13a1c734d..36a578a056bcb5 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -21,7 +21,11 @@ import { OverlayStart } from 'kibana/public'; import { EuiFieldText, EuiModalBody, EuiButton } from '@elastic/eui'; import { useState } from 'react'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { createAction, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { + ActionExecutionContext, + createAction, + UiActionsStart, +} from '../../../../src/plugins/ui_actions/public'; export const USER_TRIGGER = 'USER_TRIGGER'; export const COUNTRY_TRIGGER = 'COUNTRY_TRIGGER'; @@ -37,7 +41,7 @@ export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY'; export const showcasePluggability = createAction({ type: ACTION_SHOWCASE_PLUGGABILITY, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', - execute: async (context) => + execute: async (context: ActionExecutionContext) => alert(`Isn't that cool?! Triggered by ${context.trigger?.id} trigger`), }); diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index ab2fa47d9d04c9..26af13b4410fe6 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -21,11 +21,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from 'src/core/public'; import uuid from 'uuid'; import _ from 'lodash'; -import { - ActionByType, - ActionExecutionContext, - IncompatibleActionError, -} from '../../ui_actions_plugin'; +import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { ViewMode, PanelState, IEmbeddable } from '../../embeddable_plugin'; import { SavedObject } from '../../../../saved_objects/public'; import { PanelNotFoundError, EmbeddableInput } from '../../../../embeddable/public'; @@ -64,7 +60,7 @@ export class ClonePanelAction implements ActionByType return 'copy'; } - public async isCompatible({ embeddable }: ActionExecutionContext) { + public async isCompatible({ embeddable }: ClonePanelActionContext) { return Boolean( embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && @@ -73,7 +69,7 @@ export class ClonePanelAction implements ActionByType ); } - public async execute({ embeddable }: ActionExecutionContext) { + public async execute({ embeddable }: ClonePanelActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { throw new IncompatibleActionError(); } diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx index 0918ee8a7db7f7..d0442fbc26073d 100644 --- a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; import { IEmbeddable } from '../../embeddable_plugin'; -import { - ActionByType, - ActionExecutionContext, - IncompatibleActionError, -} from '../../ui_actions_plugin'; +import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; export const ACTION_EXPAND_PANEL = 'togglePanel'; @@ -73,11 +69,11 @@ export class ExpandPanelAction implements ActionByType) { + public async isCompatible({ embeddable }: ExpandPanelActionContext) { return Boolean(embeddable.parent && isDashboard(embeddable.parent)); } - public async execute({ embeddable }: ActionExecutionContext) { + public async execute({ embeddable }: ExpandPanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx index 1b96228efad419..5526af2f83850c 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx @@ -21,11 +21,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from 'src/core/public'; import { IEmbeddable, ViewMode, EmbeddableStart } from '../../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; -import { - ActionByType, - ActionExecutionContext, - IncompatibleActionError, -} from '../../ui_actions_plugin'; +import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; export const ACTION_REPLACE_PANEL = 'replacePanel'; @@ -66,7 +62,7 @@ export class ReplacePanelAction implements ActionByType) { + public async isCompatible({ embeddable }: ReplacePanelActionContext) { if (embeddable.getInput().viewMode) { if (embeddable.getInput().viewMode === ViewMode.VIEW) { return false; @@ -76,7 +72,7 @@ export class ReplacePanelAction implements ActionByType) { + public async execute({ embeddable }: ReplacePanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 0c65a4201d14a2..31980c3af5295b 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -36,7 +36,7 @@ export interface ActionExecutionMeta { trigger?: Trigger; } -export type ActionExecutionContext = Context & +export type ActionExecutionContext = Context & ActionExecutionMeta; export interface Action @@ -78,19 +78,19 @@ export interface Action * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. */ - isCompatible(context: ActionExecutionContext): Promise; + isCompatible(context: Context | ActionExecutionContext): Promise; /** * Executes the action. */ - execute(context: ActionExecutionContext): Promise; + execute(context: Context | ActionExecutionContext): Promise; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: ActionExecutionContext): Promise; + getHref?(context: Context | ActionExecutionContext): Promise; } /** @@ -112,19 +112,19 @@ export interface ActionDefinition * Returns a promise that resolves to true if this item is compatible given * the context and should be displayed to user, otherwise resolves to false. */ - isCompatible?(context: ActionExecutionContext): Promise; + isCompatible?(context: Context | ActionExecutionContext): Promise; /** * Executes the action. */ - execute(context: ActionExecutionContext): Promise; + execute(context: Context | ActionExecutionContext): Promise; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: ActionExecutionContext): Promise; + getHref?(context: Context | ActionExecutionContext): Promise; } export type ActionContext = A extends ActionDefinition ? Context : never; diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 49c8d08daa179d..9fcd8a32881df3 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -20,12 +20,7 @@ import { ActionInternal } from './actions/action_internal'; import { TriggerInternal } from './triggers/trigger_internal'; import { Filter } from '../../data/public'; -import { - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, - APPLY_FILTER_TRIGGER, - Trigger, -} from './triggers'; +import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; import { IEmbeddable } from '../../embeddable/public'; import { RangeSelectContext, ValueClickContext } from '../../embeddable/public'; @@ -39,11 +34,6 @@ export type TriggerId = keyof TriggerContextMapping; export type BaseContext = object; export type TriggerContext = BaseContext; -export interface BaseActionContext { - meta?: { - trigger?: Trigger; - }; -} export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; @@ -59,5 +49,5 @@ const DEFAULT_ACTION = ''; export type ActionType = keyof ActionContextMapping; export interface ActionContextMapping { - [DEFAULT_ACTION]: BaseActionContext; + [DEFAULT_ACTION]: BaseContext; } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 07bc61b2b1b6c0..67599687dd881b 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -10,7 +10,7 @@ import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/publ import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; -import { EventMeta } from '../../../../../src/plugins/ui_actions/public/actions'; +import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; function isValidUrl(url: string) { try { @@ -102,11 +102,14 @@ export class DashboardToUrlDrilldown implements Drilldown return config.url; }; - public readonly execute = async (config: Config, context: ActionContext, event: EventMeta) => { + public readonly execute = async ( + config: Config, + context: ActionExecutionContext + ) => { // Just for showcasing: // we can get trigger a which caused this drilldown execution // eslint-disable-next-line no-console - console.log(event.trigger?.id); + console.log(context.trigger?.id); const url = await this.getHref(config, context); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index ea47edf599d633..756bdf9e672aac 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -6,7 +6,7 @@ import { ActionFactoryDefinition } from '../dynamic_actions'; import { LicenseType } from '../../../licensing/public'; -import { EventMeta } from '../../../../../src/plugins/ui_actions/public/'; +import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; /** * This is a convenience interface to register a drilldown. Drilldown has @@ -93,13 +93,17 @@ export interface DrilldownDefinition< * @param config Config object that user configured this drilldown with. * @param context Object that represents context in which the underlying * `UIAction` of this drilldown is being executed in. - * @param meta object with meta information about underlying `UiAction` execution. - * For example, contains `trigger` which fired corresponding `UiAction` */ - execute(config: Config, context: ExecutionContext, meta: EventMeta): void; + execute( + config: Config, + context: ExecutionContext | ActionExecutionContext + ): void; /** * A link where drilldown should navigate on middle click or Ctrl + click. */ - getHref?(config: Config, context: ExecutionContext, meta: EventMeta): Promise; + getHref?( + config: Config, + context: ExecutionContext | ActionExecutionContext + ): Promise; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 60bad4ae90b471..95b7941b48ed35 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -67,10 +67,10 @@ export class ActionFactory< const action = this.def.create(serializedAction); return { ...action, - isCompatible: async (context: ActionContext, meta): Promise => { + isCompatible: async (context: ActionContext): Promise => { if (!this.isCompatibleLicence()) return false; if (!action.isCompatible) return true; - return action.isCompatible(context, meta); + return action.isCompatible(context); }, }; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 5d7fcacc9a6406..4afefe3006a43c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -78,10 +78,10 @@ export class DynamicActionManager { uiActions.registerAction({ ...actionDefinition, id: actionId, - isCompatible: async (context, meta) => { + isCompatible: async (context) => { if (!(await isCompatible(context))) return false; if (!actionDefinition.isCompatible) return true; - return actionDefinition.isCompatible(context, meta); + return actionDefinition.isCompatible(context); }, }); for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId); diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index e195d1ce36df1c..bd05659d59e9d8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -96,10 +96,8 @@ export class UiActionsServiceEnhancements { type: factoryId, getIconType: () => euiIcon, getDisplayName: () => serializedAction.name, - execute: async (context, meta) => await execute(serializedAction.config, context, meta), - getHref: getHref - ? async (context, meta) => getHref(serializedAction.config, context, meta) - : undefined, + execute: async (context) => await execute(serializedAction.config, context), + getHref: getHref ? async (context) => getHref(serializedAction.config, context) : undefined, }), } as ActionFactoryDefinition; From c06d4f64c9df1a95fc8ce94a6f38af08100dd725 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 15 Jul 2020 14:55:08 +0200 Subject: [PATCH 06/12] simpler --- src/plugins/ui_actions/public/actions/action.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/plugins/ui_actions/public/actions/action.test.ts b/src/plugins/ui_actions/public/actions/action.test.ts index 38ebd4fe61dfef..f9d696d3ddb5f4 100644 --- a/src/plugins/ui_actions/public/actions/action.test.ts +++ b/src/plugins/ui_actions/public/actions/action.test.ts @@ -19,14 +19,12 @@ import { createAction } from '../../../ui_actions/public'; import { ActionType } from '../types'; -import { ActionExecutionContext } from './action'; const sayHelloAction = createAction({ // Casting to ActionType is a hack - in a real situation use // declare module and add this id to ActionContextMapping. type: 'test' as ActionType, - isCompatible: ({ amICompatible }: ActionExecutionContext<{ amICompatible: boolean }>) => - Promise.resolve(amICompatible), + isCompatible: ({ amICompatible }: { amICompatible: boolean }) => Promise.resolve(amICompatible), execute: () => Promise.resolve(), }); From 4618512d284474ae05fbdc57de8246802d652c1b Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 16 Jul 2020 12:15:37 +0200 Subject: [PATCH 07/12] improve --- .../public/lib/panel/embeddable_panel.tsx | 5 ++- .../ui_actions/public/actions/action.ts | 4 +-- .../build_eui_context_menu_panels.tsx | 36 +++++++++++++++---- .../service/ui_actions_execution_service.ts | 13 +++++-- .../tests/execute_trigger_actions.test.ts | 24 ++++++++++++- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index cb02ffc470e95c..359cc3108e22e2 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -311,7 +311,10 @@ export class EmbeddablePanel extends React.Component { const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); return await buildContextMenuForActions({ - actions: sortedActions.map((action) => [action, { embeddable: this.props.embeddable }]), + actions: sortedActions.map((action) => ({ + action, + context: { embeddable: this.props.embeddable }, + })), closeMenu: this.closeMyContextMenuPanel, }); }; diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 76065e91891e72..22f0d79dcef114 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -31,7 +31,7 @@ export type ActionByType = Action * without first showing up in context menu. * false by default. */ - shouldAutoExecute?(context: Context): Promise; + shouldAutoExecute?(context: Context | ActionExecutionContext): Promise; /** * This method should return a link if this item can be clicked on. The link diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 487f93ea58e5da..d47f591ffefc15 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -30,7 +30,15 @@ export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { defaultMessage: 'Options', }); -type ActionWithContext = [Action, Context]; +interface ActionWithContext { + action: Action; + context: Context; + + /** + * Trigger that caused this action + */ + trigger?: Trigger; +} /** * Transforms an array of Actions to the shape EuiContextMenuPanel expects. @@ -67,15 +75,18 @@ async function buildEuiContextMenuPanelItems({ closeMenu: () => void; }) { const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length); - const promises = actions.map(async ([action, actionContext], index) => { - const isCompatible = await action.isCompatible(actionContext); + const promises = actions.map(async ({ action, context, trigger }, index) => { + const isCompatible = await action.isCompatible({ + ...context, + trigger, + }); if (!isCompatible) { return; } items[index] = await convertPanelActionToContextMenuItem({ action, - actionContext, + actionContext: context, closeMenu, }); }); @@ -88,10 +99,12 @@ async function buildEuiContextMenuPanelItems({ async function convertPanelActionToContextMenuItem({ action, actionContext, + trigger, closeMenu, }: { action: Action; actionContext: Context; + trigger?: Trigger; closeMenu: () => void; }): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { @@ -115,20 +128,29 @@ async function convertPanelActionToContextMenuItem({ !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys ) { event.preventDefault(); - action.execute(actionContext); + action.execute({ + ...actionContext, + trigger, + }); } else { // let browser handle navigation } } else { // not a link - action.execute(actionContext); + action.execute({ + ...actionContext, + trigger, + }); } closeMenu(); }; if (action.getHref) { - const href = await action.getHref(actionContext); + const href = await action.getHref({ + ...actionContext, + trigger, + }); if (href) { menuPanelItem.href = href; } diff --git a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts index 7393989672e9de..44326e6aa1e769 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts @@ -96,9 +96,12 @@ export class UiActionsExecutionService { }, 0); } - private async executeSingleTask({ context, action, defer }: ExecuteActionTask) { + private async executeSingleTask({ context, action, defer, trigger }: ExecuteActionTask) { try { - await action.execute(context); + await action.execute({ + ...context, + trigger, + }); defer.resolve(); } catch (e) { defer.reject(e); @@ -107,7 +110,11 @@ export class UiActionsExecutionService { private async executeMultipleActions(tasks: ExecuteActionTask[]) { const panel = await buildContextMenuForActions({ - actions: tasks.map(({ action, context }) => [action, context]), + actions: tasks.map(({ action, context, trigger }) => ({ + action, + context, + trigger, + })), title: tasks[0].trigger.title, // title of context menu is title of trigger which originated the chain closeMenu: () => { tasks.forEach((t) => t.defer.resolve()); diff --git a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts index 9af46f25b4fec4..81120990001e34 100644 --- a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts @@ -82,7 +82,7 @@ test('executes a single action mapped to a trigger', async () => { jest.runAllTimers(); expect(executeFn).toBeCalledTimes(1); - expect(executeFn).toBeCalledWith(context); + expect(executeFn).toBeCalledWith(expect.objectContaining(context)); }); test('throws an error if there are no compatible actions to execute', async () => { @@ -202,3 +202,25 @@ test("doesn't show a context menu for auto executable actions", async () => { expect(openContextMenu).toHaveBeenCalledTimes(0); }); }); + +test('passes trigger into execute', async () => { + const { setup, doStart } = uiActions; + const trigger = { + id: 'MY-TRIGGER' as TriggerId, + title: 'My trigger', + }; + const action = createTestAction<{ foo: string }>('test', () => true); + + setup.registerTrigger(trigger); + setup.addTriggerAction(trigger.id, action); + + const start = doStart(); + + const context = { foo: 'bar' }; + await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + jest.runAllTimers(); + expect(executeFn).toBeCalledWith({ + ...context, + trigger, + }); +}); From 2058792653a7df75f975506078fe5d3db7d10511 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 5 Aug 2020 15:01:12 +0200 Subject: [PATCH 08/12] take different direction --- examples/ui_actions_explorer/public/app.tsx | 6 ++-- .../lib/actions/apply_filter_action.test.ts | 3 ++ .../public/lib/panel/embeddable_panel.tsx | 5 +-- .../add_panel/add_panel_action.ts | 12 ++++--- .../lib/panel/panel_header/panel_header.tsx | 14 ++++---- .../test_samples/actions/say_hello_action.tsx | 4 +-- .../public/tests/apply_filter_action.test.ts | 2 +- .../ui_actions/public/actions/action.test.ts | 12 +++++-- .../ui_actions/public/actions/action.ts | 36 +++++++++++-------- .../public/actions/create_action.ts | 2 +- .../build_eui_context_menu_panels.tsx | 20 ++++++++--- src/plugins/ui_actions/public/index.ts | 7 +++- .../service/ui_actions_execution_service.ts | 4 +-- .../public/service/ui_actions_service.ts | 2 +- .../flyout_edit_drilldown.tsx | 5 +-- .../panel_actions/get_csv_panel_action.tsx | 7 ++-- .../public/custom_time_range_action.tsx | 7 ++-- 17 files changed, 94 insertions(+), 54 deletions(-) diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index 1b0667962a3c2f..d59309f0068385 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -97,9 +97,9 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { }); uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); setConfirmationText( - `You've successfully added a new action: ${dynamicAction.getDisplayName( - {} - )}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` + `You've successfully added a new action: ${dynamicAction.getDisplayName({ + trigger: uiActionsApi.getTrigger(HELLO_WORLD_TRIGGER_ID), + })}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` ); }} > diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts index 636ce3e623c5bf..3899e45e87de18 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts @@ -51,6 +51,7 @@ describe('isCompatible()', () => { }), } as any, filters: [], + trigger: null, }); expect(result).toBe(true); }); @@ -66,6 +67,7 @@ describe('isCompatible()', () => { }), } as any, filters: [], + trigger: null, }); expect(result).toBe(false); }); @@ -119,6 +121,7 @@ describe('execute()', () => { await action.execute({ embeddable, filters: ['FILTER' as any], + trigger: null, }); expect(root.updateInput).toHaveBeenCalledTimes(1); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 359cc3108e22e2..c80c6d04bc5281 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -20,7 +20,7 @@ import { EuiContextMenuPanelDescriptor, EuiPanel, htmlIdGenerator } from '@elast import classNames from 'classnames'; import React from 'react'; import { Subscription } from 'rxjs'; -import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions'; +import { buildContextMenuForActions, UiActionsService, Action, createAction } from '../ui_actions'; import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; @@ -306,7 +306,7 @@ export class EmbeddablePanel extends React.Component { this.props.application, this.props.stateTransfer ), - ]; + ].map(createAction); const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); @@ -314,6 +314,7 @@ export class EmbeddablePanel extends React.Component { actions: sortedActions.map((action) => ({ action, context: { embeddable: this.props.embeddable }, + trigger: null, })), closeMenu: this.closeMyContextMenuPanel, }); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index f3a483bb4bda4b..2fc0abc9dc4803 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { Action } from 'src/plugins/ui_actions/public'; +import { UiActionsActionDefinition as ActionDefinition } from 'src/plugins/ui_actions/public'; import { NotificationsStart, OverlayStart } from 'src/core/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { ViewMode } from '../../../../types'; @@ -30,7 +30,7 @@ interface ActionContext { embeddable: IContainer; } -export class AddPanelAction implements Action { +export class AddPanelAction implements ActionDefinition { public readonly type = ACTION_ADD_PANEL; public readonly id = ACTION_ADD_PANEL; @@ -52,12 +52,14 @@ export class AddPanelAction implements Action { return 'plusInCircleFilled'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible(context: ActionContext) { + const { embeddable } = context; return embeddable.getIsContainer() && embeddable.getInput().viewMode === ViewMode.EDIT; } - public async execute({ embeddable }: ActionContext) { - if (!embeddable.getIsContainer() || !(await this.isCompatible({ embeddable }))) { + public async execute(context: ActionContext) { + const { embeddable } = context; + if (!embeddable.getIsContainer() || !(await this.isCompatible(context))) { throw new Error('Context is incompatible'); } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 7b66f29cc27267..e237b8e0540bfd 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -49,11 +49,11 @@ function renderBadges(badges: Array>, embeddable: IEmb badge.execute({ embeddable })} - onClickAriaLabel={badge.getDisplayName({ embeddable })} + iconType={badge.getIconType({ embeddable, trigger: null })} + onClick={() => badge.execute({ embeddable, trigger: null })} + onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: null })} > - {badge.getDisplayName({ embeddable })} + {badge.getDisplayName({ embeddable, trigger: null })} )); } @@ -70,14 +70,14 @@ function renderNotifications( data-test-subj={`embeddablePanelNotification-${notification.id}`} key={notification.id} style={{ marginTop: '4px', marginRight: '4px' }} - onClick={() => notification.execute(context)} + onClick={() => notification.execute({ ...context, trigger: null })} > - {notification.getDisplayName(context)} + {notification.getDisplayName({ ...context, trigger: null })} ); if (notification.getDisplayNameTooltip) { - const tooltip = notification.getDisplayNameTooltip(context); + const tooltip = notification.getDisplayNameTooltip({ ...context, trigger: null }); if (tooltip) { badge = ( diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx index 0612b838a6ce70..968caf67b18262 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { ActionByType, IncompatibleActionError, ActionType } from '../../ui_actions'; +import { IncompatibleActionError, ActionType, ActionDefinitionByType } from '../../ui_actions'; import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables'; // Casting to ActionType is a hack - in a real situation use @@ -42,7 +42,7 @@ export interface SayHelloActionContext { message?: string; } -export class SayHelloAction implements ActionByType { +export class SayHelloAction implements ActionDefinitionByType { public readonly type = SAY_HELLO_ACTION; public readonly id = SAY_HELLO_ACTION; diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index ec92f334267f58..f82d557cc77528 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -86,7 +86,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a query: { match: { extension: { query: 'foo' } } }, }; - await applyFilterAction.execute({ embeddable, filters: [filter] }); + await applyFilterAction.execute({ embeddable, filters: [filter], trigger: null }); expect(root.getInput().filters.length).toBe(1); expect(node1.getInput().filters.length).toBe(1); expect(embeddable.getInput().filters.length).toBe(1); diff --git a/src/plugins/ui_actions/public/actions/action.test.ts b/src/plugins/ui_actions/public/actions/action.test.ts index f9d696d3ddb5f4..287a6e5a198b5e 100644 --- a/src/plugins/ui_actions/public/actions/action.test.ts +++ b/src/plugins/ui_actions/public/actions/action.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createAction } from '../../../ui_actions/public'; +import { ActionExecutionContext, createAction } from '../../../ui_actions/public'; import { ActionType } from '../types'; const sayHelloAction = createAction({ @@ -29,11 +29,17 @@ const sayHelloAction = createAction({ }); test('action is not compatible based on context', async () => { - const isCompatible = await sayHelloAction.isCompatible({ amICompatible: false }); + const isCompatible = await sayHelloAction.isCompatible({ + amICompatible: false, + trigger: null, + } as ActionExecutionContext); expect(isCompatible).toBe(false); }); test('action is compatible based on context', async () => { - const isCompatible = await sayHelloAction.isCompatible({ amICompatible: true }); + const isCompatible = await sayHelloAction.isCompatible({ + amICompatible: true, + trigger: null, + } as ActionExecutionContext); expect(isCompatible).toBe(true); }); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 22f0d79dcef114..e0aac3db0c0e04 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -23,6 +23,9 @@ import { Presentable } from '../util/presentable'; import { Trigger } from '../triggers'; export type ActionByType = Action; +export type ActionDefinitionByType = ActionDefinition< + ActionContextMapping[T] +>; /** * During action execution we can provide additional information, @@ -31,16 +34,19 @@ export type ActionByType = Action = Context & ActionExecutionMeta; +export type ActionDefinitionContext = + | Context + | ActionExecutionContext; + export interface Action - extends Partial> { + extends Partial>> { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. @@ -60,51 +66,51 @@ export interface Action /** * Optional EUI icon type that can be displayed along with the title. */ - getIconType(context: Context): string | undefined; + getIconType(context: ActionExecutionContext): string | undefined; /** * Returns a title to be displayed to the user. * @param context */ - getDisplayName(context: Context): string; + getDisplayName(context: ActionExecutionContext): string; /** * `UiComponent` to render when displaying this action as a context menu item. * If not provided, `getDisplayName` will be used instead. */ - MenuItem?: UiComponent<{ context: Context }>; + MenuItem?: UiComponent<{ context: ActionExecutionContext }>; /** * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. */ - isCompatible(context: Context | ActionExecutionContext): Promise; + isCompatible(context: ActionExecutionContext): Promise; /** * Executes the action. */ - execute(context: Context | ActionExecutionContext): Promise; + execute(context: ActionExecutionContext): Promise; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: Context | ActionExecutionContext): Promise; + getHref?(context: ActionExecutionContext): Promise; /** * Determines if action should be executed automatically, * without first showing up in context menu. * false by default. */ - shouldAutoExecute?(context: Context): Promise; + shouldAutoExecute?(context: ActionExecutionContext): Promise; } /** * A convenience interface used to register an action. */ export interface ActionDefinition - extends Partial> { + extends Partial>> { /** * ID of the action that uniquely identifies this action in the actions registry. */ @@ -119,26 +125,26 @@ export interface ActionDefinition * Returns a promise that resolves to true if this item is compatible given * the context and should be displayed to user, otherwise resolves to false. */ - isCompatible?(context: Context | ActionExecutionContext): Promise; + isCompatible?(context: ActionDefinitionContext): Promise; /** * Executes the action. */ - execute(context: Context | ActionExecutionContext): Promise; + execute(context: ActionDefinitionContext): Promise; /** * Determines if action should be executed automatically, * without first showing up in context menu. * false by default. */ - shouldAutoExecute?(context: Context | ActionExecutionContext): Promise; + shouldAutoExecute?(context: ActionDefinitionContext): Promise; /** * This method should return a link if this item can be clicked on. The link * is used to navigate user if user middle-clicks it or Ctrl + clicks or * right-clicks and selects "Open in new tab". */ - getHref?(context: Context | ActionExecutionContext): Promise; + getHref?(context: ActionDefinitionContext): Promise; } export type ActionContext = A extends ActionDefinition ? Context : never; diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index dea21678eccea6..3fb568e7de19c6 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -33,7 +33,7 @@ export function createAction( return { getIconType: () => undefined, order: 0, - id: action.type, + id: action.id ?? action.type, isCompatible: () => Promise.resolve(true), getDisplayName: () => '', ...action, diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index d47f591ffefc15..680e6451f7394f 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -37,7 +37,7 @@ interface ActionWithContext { /** * Trigger that caused this action */ - trigger?: Trigger; + trigger: Trigger | null; } /** @@ -87,6 +87,7 @@ async function buildEuiContextMenuPanelItems({ items[index] = await convertPanelActionToContextMenuItem({ action, actionContext: context, + trigger, closeMenu, }); }); @@ -104,16 +105,25 @@ async function convertPanelActionToContextMenuItem({ }: { action: Action; actionContext: Context; - trigger?: Trigger; + trigger: Trigger | null; closeMenu: () => void; }): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { name: action.MenuItem ? React.createElement(uiToReactComponent(action.MenuItem), { - context: actionContext, + context: { + ...actionContext, + trigger, + }, }) - : action.getDisplayName(actionContext), - icon: action.getIconType(actionContext), + : action.getDisplayName({ + ...actionContext, + trigger, + }), + icon: action.getIconType({ + ...actionContext, + trigger, + }), panel: _.get(action, 'childContextMenuPanel.id'), 'data-test-subj': `embeddablePanelAction-${action.id}`, }; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 34b6fc3ba0771e..d76ca124ead2c6 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -45,4 +45,9 @@ export { applyFilterTrigger, } from './triggers'; export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types'; -export { ActionByType, ActionExecutionContext, ActionExecutionMeta } from './actions'; +export { + ActionByType, + ActionDefinitionByType, + ActionExecutionContext, + ActionExecutionMeta, +} from './actions'; diff --git a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts index 44326e6aa1e769..df89c9c2f70e9d 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts @@ -46,7 +46,7 @@ export class UiActionsExecutionService { context: BaseContext; trigger: Trigger; }): Promise { - const shouldBatch = !(await action.shouldAutoExecute?.(context)) ?? false; + const shouldBatch = !(await action.shouldAutoExecute?.({ ...context, trigger })) ?? false; const task: ExecuteActionTask = { action, context, @@ -59,7 +59,7 @@ export class UiActionsExecutionService { } else { this.pendingTasks.add(task); try { - await action.execute(context); + await action.execute({ ...context, trigger }); this.pendingTasks.delete(task); } catch (e) { this.pendingTasks.delete(task); diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index c3094b7dc5c073..8fc5c20dddb05c 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -142,7 +142,7 @@ export class UiActionsService { triggerId: T, // The action can accept partial or no context, but if it needs context not provided // by this type of trigger, typescript will complain. yay! - action: Action + action: ActionDefinition | Action ): void => { if (!this.actions.has(action.id)) this.registerAction(action); this.attachAction(triggerId, action.id); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index af1ae67454463e..6be77c1c41cf75 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; +import { ActionDefinitionByType } from '../../../../../../../../src/plugins/ui_actions/public'; import { reactToUiComponent, toMountPoint, @@ -23,7 +23,8 @@ export interface FlyoutEditDrilldownParams { start: StartServicesGetter>; } -export class FlyoutEditDrilldownAction implements ActionByType { +export class FlyoutEditDrilldownAction + implements ActionDefinitionByType { public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; public order = 10; diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index d0800c7b24fef4..30025dce18c0bf 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -8,7 +8,10 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; -import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; +import { + UiActionsActionDefinition as ActionDefinition, + IncompatibleActionError, +} from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { checkLicense } from '../lib/license_check'; @@ -30,7 +33,7 @@ interface ActionContext { embeddable: ISearchEmbeddable; } -export class GetCsvReportPanelAction implements Action { +export class GetCsvReportPanelAction implements ActionDefinition { private isDownloading: boolean; public readonly type = ''; public readonly id = CSV_REPORTING_ACTION; diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 5d9804d2a5c33c..259fe5c774c4b9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { ActionByType, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { + ActionDefinitionByType, + IncompatibleActionError, +} from '../../../../src/plugins/ui_actions/public'; import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { OpenModal, CommonlyUsedRange } from './types'; @@ -38,7 +41,7 @@ export interface TimeRangeActionContext { embeddable: Embeddable; } -export class CustomTimeRangeAction implements ActionByType { +export class CustomTimeRangeAction implements ActionDefinitionByType { public readonly type = CUSTOM_TIME_RANGE; private openModal: OpenModal; private dateFormat?: string; From c460608909399c417c3c7faefb7d31dad65c6892 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 5 Aug 2020 16:44:11 +0200 Subject: [PATCH 09/12] fix --- src/plugins/ui_actions/public/actions/create_action.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index 3fb568e7de19c6..7382e466f6a85f 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -31,11 +31,11 @@ export function createAction( action: ActionDefinitionByType ): ActionByType { return { - getIconType: () => undefined, - order: 0, + getIconType: action.getIconType ?? (() => undefined), + order: action.order ?? 0, id: action.id ?? action.type, - isCompatible: () => Promise.resolve(true), - getDisplayName: () => '', + isCompatible: action.isCompatible ?? (() => Promise.resolve(true)), + getDisplayName: action.getDisplayName ?? (() => ''), ...action, } as ActionByType; } From 4393299ba2882e4f14d6731c25339e9e9ac60a0a Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Aug 2020 13:33:57 +0200 Subject: [PATCH 10/12] improve --- .../lib/actions/apply_filter_action.test.ts | 7 ++--- .../public/lib/panel/embeddable_panel.tsx | 3 ++- .../lib/panel/panel_header/panel_header.tsx | 19 +++++++------ .../public/tests/apply_filter_action.test.ts | 3 ++- .../ui_actions/public/actions/action.test.ts | 5 ++-- .../ui_actions/public/actions/action.ts | 10 ++++++- .../build_eui_context_menu_panels.tsx | 4 +-- .../public/triggers/default_trigger.ts | 27 +++++++++++++++++++ .../ui_actions/public/triggers/index.ts | 1 + src/plugins/ui_actions/public/types.ts | 9 ++++--- .../flyout_edit_drilldown.tsx | 5 ++-- 11 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 src/plugins/ui_actions/public/triggers/default_trigger.ts diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts index 3899e45e87de18..88c1a5917e609c 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts @@ -19,6 +19,7 @@ import { createFilterAction } from './apply_filter_action'; import { expectErrorAsync } from '../../tests/helpers'; +import { defaultTrigger } from '../../../../ui_actions/public/triggers'; test('has ACTION_APPLY_FILTER type and id', () => { const action = createFilterAction(); @@ -51,7 +52,7 @@ describe('isCompatible()', () => { }), } as any, filters: [], - trigger: null, + trigger: defaultTrigger, }); expect(result).toBe(true); }); @@ -67,7 +68,7 @@ describe('isCompatible()', () => { }), } as any, filters: [], - trigger: null, + trigger: defaultTrigger, }); expect(result).toBe(false); }); @@ -121,7 +122,7 @@ describe('execute()', () => { await action.execute({ embeddable, filters: ['FILTER' as any], - trigger: null, + trigger: defaultTrigger, }); expect(root.updateInput).toHaveBeenCalledTimes(1); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index c80c6d04bc5281..df7492b6d29ef1 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -30,6 +30,7 @@ import { PANEL_BADGE_TRIGGER, PANEL_NOTIFICATION_TRIGGER, EmbeddableContext, + contextMenuTrigger, } from '../triggers'; import { IEmbeddable, EmbeddableOutput, EmbeddableError } from '../embeddables/i_embeddable'; import { ViewMode } from '../types'; @@ -314,7 +315,7 @@ export class EmbeddablePanel extends React.Component { actions: sortedActions.map((action) => ({ action, context: { embeddable: this.props.embeddable }, - trigger: null, + trigger: contextMenuTrigger, })), closeMenu: this.closeMyContextMenuPanel, }); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index e237b8e0540bfd..34411b7d5b9702 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { Action } from 'src/plugins/ui_actions/public'; import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; -import { EmbeddableContext } from '../../triggers'; +import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers'; export interface PanelHeaderProps { title?: string; @@ -49,11 +49,11 @@ function renderBadges(badges: Array>, embeddable: IEmb badge.execute({ embeddable, trigger: null })} - onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: null })} + iconType={badge.getIconType({ embeddable, trigger: panelBadgeTrigger })} + onClick={() => badge.execute({ embeddable, trigger: panelBadgeTrigger })} + onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })} > - {badge.getDisplayName({ embeddable, trigger: null })} + {badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })} )); } @@ -70,14 +70,17 @@ function renderNotifications( data-test-subj={`embeddablePanelNotification-${notification.id}`} key={notification.id} style={{ marginTop: '4px', marginRight: '4px' }} - onClick={() => notification.execute({ ...context, trigger: null })} + onClick={() => notification.execute({ ...context, trigger: panelNotificationTrigger })} > - {notification.getDisplayName({ ...context, trigger: null })} + {notification.getDisplayName({ ...context, trigger: panelNotificationTrigger })} ); if (notification.getDisplayNameTooltip) { - const tooltip = notification.getDisplayNameTooltip({ ...context, trigger: null }); + const tooltip = notification.getDisplayNameTooltip({ + ...context, + trigger: panelNotificationTrigger, + }); if (tooltip) { badge = ( diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index f82d557cc77528..fdf272f42ec60f 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -32,6 +32,7 @@ import { } from '../lib/test_samples'; // eslint-disable-next-line import { esFilters } from '../../../data/public'; +import { applyFilterTrigger } from '../../../ui_actions/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { const { doStart, setup } = testPlugin(); @@ -86,7 +87,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a query: { match: { extension: { query: 'foo' } } }, }; - await applyFilterAction.execute({ embeddable, filters: [filter], trigger: null }); + await applyFilterAction.execute({ embeddable, filters: [filter], trigger: applyFilterTrigger }); expect(root.getInput().filters.length).toBe(1); expect(node1.getInput().filters.length).toBe(1); expect(embeddable.getInput().filters.length).toBe(1); diff --git a/src/plugins/ui_actions/public/actions/action.test.ts b/src/plugins/ui_actions/public/actions/action.test.ts index 287a6e5a198b5e..1f76223a0d7c4b 100644 --- a/src/plugins/ui_actions/public/actions/action.test.ts +++ b/src/plugins/ui_actions/public/actions/action.test.ts @@ -19,6 +19,7 @@ import { ActionExecutionContext, createAction } from '../../../ui_actions/public'; import { ActionType } from '../types'; +import { defaultTrigger } from '../triggers'; const sayHelloAction = createAction({ // Casting to ActionType is a hack - in a real situation use @@ -31,7 +32,7 @@ const sayHelloAction = createAction({ test('action is not compatible based on context', async () => { const isCompatible = await sayHelloAction.isCompatible({ amICompatible: false, - trigger: null, + trigger: defaultTrigger, } as ActionExecutionContext); expect(isCompatible).toBe(false); }); @@ -39,7 +40,7 @@ test('action is not compatible based on context', async () => { test('action is compatible based on context', async () => { const isCompatible = await sayHelloAction.isCompatible({ amICompatible: true, - trigger: null, + trigger: defaultTrigger, } as ActionExecutionContext); expect(isCompatible).toBe(true); }); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index e0aac3db0c0e04..8005dadd8f5ef3 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -35,12 +35,20 @@ export interface ActionExecutionMeta { /** * Trigger that executed the action */ - trigger: Trigger | null; + trigger: Trigger; } +/** + * Action methods are executed with Context from trigger + {@link ActionExecutionMeta} + */ export type ActionExecutionContext = Context & ActionExecutionMeta; +/** + * Simplified action context for {@link ActionDefinition} + * When defining action consumer may use either it's own Context + * or an ActionExecutionContext to get access to {@link ActionExecutionMeta} params + */ export type ActionDefinitionContext = | Context | ActionExecutionContext; diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 680e6451f7394f..b44a07273f4a92 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -37,7 +37,7 @@ interface ActionWithContext { /** * Trigger that caused this action */ - trigger: Trigger | null; + trigger: Trigger; } /** @@ -105,7 +105,7 @@ async function convertPanelActionToContextMenuItem({ }: { action: Action; actionContext: Context; - trigger: Trigger | null; + trigger: Trigger; closeMenu: () => void; }): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { diff --git a/src/plugins/ui_actions/public/triggers/default_trigger.ts b/src/plugins/ui_actions/public/triggers/default_trigger.ts new file mode 100644 index 00000000000000..74be0243bdac58 --- /dev/null +++ b/src/plugins/ui_actions/public/triggers/default_trigger.ts @@ -0,0 +1,27 @@ +/* + * 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 { Trigger } from '.'; + +export const DEFAULT_TRIGGER = ''; +export const defaultTrigger: Trigger<''> = { + id: DEFAULT_TRIGGER, + title: 'Unknown', + description: 'Unknown trigger.', +}; diff --git a/src/plugins/ui_actions/public/triggers/index.ts b/src/plugins/ui_actions/public/triggers/index.ts index a5bf9e1822941d..dbc54163c5af56 100644 --- a/src/plugins/ui_actions/public/triggers/index.ts +++ b/src/plugins/ui_actions/public/triggers/index.ts @@ -23,3 +23,4 @@ export * from './trigger_internal'; export * from './select_range_trigger'; export * from './value_click_trigger'; export * from './apply_filter_trigger'; +export * from './default_trigger'; diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 5631441cf9a1bf..dcf0bfb14d5385 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -19,7 +19,12 @@ import { ActionInternal } from './actions/action_internal'; import { TriggerInternal } from './triggers/trigger_internal'; -import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + APPLY_FILTER_TRIGGER, + DEFAULT_TRIGGER, +} from './triggers'; import type { RangeSelectContext, ValueClickContext } from '../../embeddable/public'; import type { ApplyGlobalFilterActionContext } from '../../data/public'; @@ -27,8 +32,6 @@ export type TriggerRegistry = Map>; export type ActionRegistry = Map; export type TriggerToActionsRegistry = Map; -const DEFAULT_TRIGGER = ''; - export type TriggerId = keyof TriggerContextMapping; export type BaseContext = object; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 6be77c1c41cf75..af1ae67454463e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { ActionDefinitionByType } from '../../../../../../../../src/plugins/ui_actions/public'; +import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; import { reactToUiComponent, toMountPoint, @@ -23,8 +23,7 @@ export interface FlyoutEditDrilldownParams { start: StartServicesGetter>; } -export class FlyoutEditDrilldownAction - implements ActionDefinitionByType { +export class FlyoutEditDrilldownAction implements ActionByType { public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; public order = 10; From bf563fa2e5d7fb7dc87abc12fa636aab27d49691 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Aug 2020 14:04:08 +0200 Subject: [PATCH 11/12] improve --- .../embeddable/public/lib/panel/embeddable_panel.tsx | 4 ++-- .../panel_actions/add_panel/add_panel_action.ts | 8 ++++---- src/plugins/ui_actions/public/actions/create_action.ts | 10 +++++----- .../ui_actions/public/service/ui_actions_service.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index df7492b6d29ef1..d8659680dceb95 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -20,7 +20,7 @@ import { EuiContextMenuPanelDescriptor, EuiPanel, htmlIdGenerator } from '@elast import classNames from 'classnames'; import React from 'react'; import { Subscription } from 'rxjs'; -import { buildContextMenuForActions, UiActionsService, Action, createAction } from '../ui_actions'; +import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions'; import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; @@ -307,7 +307,7 @@ export class EmbeddablePanel extends React.Component { this.props.application, this.props.stateTransfer ), - ].map(createAction); + ]; const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index 2fc0abc9dc4803..63575273bbf626 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { UiActionsActionDefinition as ActionDefinition } from 'src/plugins/ui_actions/public'; +import { Action, ActionExecutionContext } from 'src/plugins/ui_actions/public'; import { NotificationsStart, OverlayStart } from 'src/core/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { ViewMode } from '../../../../types'; @@ -30,7 +30,7 @@ interface ActionContext { embeddable: IContainer; } -export class AddPanelAction implements ActionDefinition { +export class AddPanelAction implements Action { public readonly type = ACTION_ADD_PANEL; public readonly id = ACTION_ADD_PANEL; @@ -52,12 +52,12 @@ export class AddPanelAction implements ActionDefinition { return 'plusInCircleFilled'; } - public async isCompatible(context: ActionContext) { + public async isCompatible(context: ActionExecutionContext) { const { embeddable } = context; return embeddable.getIsContainer() && embeddable.getInput().viewMode === ViewMode.EDIT; } - public async execute(context: ActionContext) { + public async execute(context: ActionExecutionContext) { const { embeddable } = context; if (!embeddable.getIsContainer() || !(await this.isCompatible(context))) { throw new Error('Context is incompatible'); diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index 7382e466f6a85f..dea21678eccea6 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -31,11 +31,11 @@ export function createAction( action: ActionDefinitionByType ): ActionByType { return { - getIconType: action.getIconType ?? (() => undefined), - order: action.order ?? 0, - id: action.id ?? action.type, - isCompatible: action.isCompatible ?? (() => Promise.resolve(true)), - getDisplayName: action.getDisplayName ?? (() => ''), + getIconType: () => undefined, + order: 0, + id: action.type, + isCompatible: () => Promise.resolve(true), + getDisplayName: () => '', ...action, } as ActionByType; } diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 8fc5c20dddb05c..6028177964fb77 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -142,7 +142,7 @@ export class UiActionsService { triggerId: T, // The action can accept partial or no context, but if it needs context not provided // by this type of trigger, typescript will complain. yay! - action: ActionDefinition | Action + action: ActionDefinition | Action // TODO: remove `Action` https://github.com/elastic/kibana/issues/74501 ): void => { if (!this.actions.has(action.id)) this.registerAction(action); this.attachAction(triggerId, action.id); From 0a3aed6be72043e3468638c6c351049452c380b8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 10 Aug 2020 11:43:39 +0200 Subject: [PATCH 12/12] fix typescript --- .../panel_actions/add_panel/add_panel_action.test.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx index d8def3147e52c9..0361939fd07e6a 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -31,6 +31,7 @@ import { ContactCardEmbeddable } from '../../../../test_samples'; import { esFilters, Filter } from '../../../../../../../../plugins/data/public'; import { EmbeddableStart } from '../../../../../plugin'; import { embeddablePluginMock } from '../../../../../mocks'; +import { defaultTrigger } from '../../../../../../../ui_actions/public/triggers'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); @@ -85,7 +86,9 @@ test('Is not compatible when container is in view mode', async () => { () => null ); container.updateInput({ viewMode: ViewMode.VIEW }); - expect(await addPanelAction.isCompatible({ embeddable: container })).toBe(false); + expect( + await addPanelAction.isCompatible({ embeddable: container, trigger: defaultTrigger }) + ).toBe(false); }); test('Is not compatible when embeddable is not a container', async () => { @@ -94,7 +97,7 @@ test('Is not compatible when embeddable is not a container', async () => { test('Is compatible when embeddable is a parent and in edit mode', async () => { container.updateInput({ viewMode: ViewMode.EDIT }); - expect(await action.isCompatible({ embeddable: container })).toBe(true); + expect(await action.isCompatible({ embeddable: container, trigger: defaultTrigger })).toBe(true); }); test('Execute throws an error when called with an embeddable that is not a container', async () => { @@ -108,6 +111,7 @@ test('Execute throws an error when called with an embeddable that is not a conta }, {} as any ), + trigger: defaultTrigger, } as any); } await expect(check()).rejects.toThrow(Error); @@ -116,6 +120,7 @@ test('Execute does not throw an error when called with a compatible container', container.updateInput({ viewMode: ViewMode.EDIT }); await action.execute({ embeddable: container, + trigger: defaultTrigger, }); });