diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index e7ca5ea803701..a3d7469fa7ccf 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -25,6 +25,7 @@ import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; +import { EmbeddableVisTriggerContext } from 'src/plugins/embeddable/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -40,8 +41,8 @@ import { EmbeddableOutput, Embeddable, Container, - VALUE_CLICK_TRIGGER, - SELECT_RANGE_TRIGGER, + selectRangeTrigger, + valueClickTrigger, } from '../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; @@ -301,13 +302,14 @@ export class VisualizeEmbeddable extends Embeddable { - const triggerContext: Trigger = { - id: CONTEXT_MENU_TRIGGER, - title: 'Context menu', - description: 'Triggered on top-right corner context-menu select.', - }; - const triggerFilter: Trigger = { - id: APPLY_FILTER_TRIGGER, - title: 'Filter click', - description: 'Triggered when user applies filter to an embeddable.', - }; - const triggerBadge: Trigger = { - id: PANEL_BADGE_TRIGGER, - title: 'Panel badges', - description: 'Actions appear in title bar when an embeddable loads in a panel', - }; - const selectRangeTrigger: Trigger = { - id: SELECT_RANGE_TRIGGER, - title: 'Select range', - description: 'Applies a range filter', - }; - const valueClickTrigger: Trigger = { - id: VALUE_CLICK_TRIGGER, - title: 'Value clicked', - description: 'Value was clicked', - }; + uiActions.registerTrigger(contextMenuTrigger); + uiActions.registerTrigger(applyFilterTrigger); + uiActions.registerTrigger(panelBadgeTrigger); + uiActions.registerTrigger(selectRangeTrigger); + uiActions.registerTrigger(valueClickTrigger); + const actionApplyFilter = createFilterAction(); - uiActions.registerTrigger(triggerContext); - uiActions.registerTrigger(triggerFilter); uiActions.registerAction(actionApplyFilter); - uiActions.registerTrigger(triggerBadge); - uiActions.registerTrigger(selectRangeTrigger); - uiActions.registerTrigger(valueClickTrigger); - // uiActions.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index b0e14a04a9944..2eafe16442e18 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -23,18 +23,17 @@ import { PluginInitializerContext } from 'src/core/public'; import { EmbeddablePublicPlugin } from './plugin'; export { + Adapters, ADD_PANEL_ACTION_ID, + AddPanelAction, APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, - PANEL_BADGE_TRIGGER, - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, - Adapters, - AddPanelAction, - CONTEXT_MENU_TRIGGER, + applyFilterTrigger, Container, ContainerInput, ContainerOutput, + CONTEXT_MENU_TRIGGER, + contextMenuTrigger, EDIT_PANEL_ACTION_ID, EditPanelAction, Embeddable, @@ -42,25 +41,32 @@ export { EmbeddableChildPanelProps, EmbeddableFactory, EmbeddableFactoryNotFoundError, + EmbeddableFactoryRenderer, EmbeddableInput, EmbeddableInstanceConfiguration, EmbeddableOutput, EmbeddablePanel, + EmbeddableRoot, + EmbeddableVisTriggerContext, ErrorEmbeddable, GetEmbeddableFactories, GetEmbeddableFactory, IContainer, IEmbeddable, + isErrorEmbeddable, + openAddPanelFlyout, OutputSpec, + PANEL_BADGE_TRIGGER, + panelBadgeTrigger, PanelNotFoundError, PanelState, PropertySpec, + SELECT_RANGE_TRIGGER, + selectRangeTrigger, + VALUE_CLICK_TRIGGER, + valueClickTrigger, ViewMode, - isErrorEmbeddable, - openAddPanelFlyout, withEmbeddableSubscription, - EmbeddableFactoryRenderer, - EmbeddableRoot, } from './lib'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/embeddable/public/lib/triggers/index.ts b/src/plugins/embeddable/public/lib/triggers/index.ts index 72565b3f527ad..4f981562a49ba 100644 --- a/src/plugins/embeddable/public/lib/triggers/index.ts +++ b/src/plugins/embeddable/public/lib/triggers/index.ts @@ -17,8 +17,4 @@ * under the License. */ -export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; -export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; -export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; -export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; -export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; +export * from './triggers'; diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts new file mode 100644 index 0000000000000..491d9e730eb75 --- /dev/null +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -0,0 +1,65 @@ +/* + * 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 '../../../../ui_actions/public'; +import { IEmbeddable } from '..'; + +export interface EmbeddableVisTriggerContext { + embeddable: IEmbeddable; + timeFieldName: string; + data: { + e: MouseEvent; + data: unknown; + }; +} + +export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; +export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = { + id: SELECT_RANGE_TRIGGER, + title: 'Select range', + description: 'Applies a range filter', +}; + +export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; +export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = { + id: VALUE_CLICK_TRIGGER, + title: 'Value clicked', + description: 'Value was clicked', +}; + +export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; +export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { + id: CONTEXT_MENU_TRIGGER, + title: 'Context menu', + description: 'Triggered on top-right corner context-menu select.', +}; + +export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; +export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { + id: APPLY_FILTER_TRIGGER, + title: 'Filter click', + description: 'Triggered when user applies filter to an embeddable.', +}; + +export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; +export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { + id: PANEL_BADGE_TRIGGER, + title: 'Panel badges', + description: 'Actions appear in title bar when an embeddable loads in a panel', +}; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 83a08b11fa4c2..1ce48d5460b2e 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -29,7 +29,8 @@ export { UiActionsSetup, UiActionsStart } from './plugin'; export { UiActionsServiceParams, UiActionsService } from './service'; export { Action, createAction, IncompatibleActionError } from './actions'; export { buildContextMenuForActions } from './context_menu'; -export { Trigger } from './triggers'; +export { Trigger, TriggerContext } from './triggers'; +export { TriggerContextMapping } from './types'; /** * @deprecated diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts index 2bbe106c49a25..8963ba4ddb005 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts @@ -68,7 +68,7 @@ describe('UiActionsService', () => { const trigger = service.getTrigger('bar'); - expect(trigger).toEqual({ + expect(trigger).toMatchObject({ description: 'foo', id: 'bar', title: 'baz', @@ -345,8 +345,9 @@ describe('UiActionsService', () => { id: 'bar', title: 'baz', }); + const triggerContract = service.getTrigger('bar'); - expect(triggers.get('bar')).toEqual({ + expect(triggerContract).toMatchObject({ description: 'foo', id: 'bar', title: 'baz', 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 a62d2aa356435..ae409830bbb6e 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -17,10 +17,11 @@ * under the License. */ -import { TriggerRegistry, ActionRegistry, TriggerToActionsRegistry } from '../types'; +import { TriggerRegistry, ActionRegistry, TriggerToActionsRegistry, TriggerId } from '../types'; import { Action } from '../actions'; -import { Trigger } from '../triggers/trigger'; -import { buildContextMenuForActions, openContextMenu } from '../context_menu'; +import { Trigger, TriggerContext } from '../triggers/trigger'; +import { TriggerInternal } from '../triggers/trigger_internal'; +import { TriggerContract } from '../triggers/trigger_contract'; export interface UiActionsServiceParams { readonly triggers?: TriggerRegistry; @@ -52,18 +53,20 @@ export class UiActionsService { throw new Error(`Trigger [trigger.id = ${trigger.id}] already registered.`); } - this.triggers.set(trigger.id, trigger); + const triggerInternal = new TriggerInternal(this, trigger); + + this.triggers.set(trigger.id, triggerInternal); this.triggerToActions.set(trigger.id, []); }; - public readonly getTrigger = (id: string) => { - const trigger = this.triggers.get(id); + public readonly getTrigger = (triggerId: T): TriggerContract => { + const trigger = this.triggers.get(triggerId as string); if (!trigger) { - throw new Error(`Trigger [triggerId = ${id}] does not exist.`); + throw new Error(`Trigger [triggerId = ${triggerId}] does not exist.`); } - return trigger; + return trigger.contract; }; public readonly registerAction = (action: Action) => { @@ -128,41 +131,17 @@ export class UiActionsService { ); }; - private async executeSingleAction(action: Action, actionContext: A) { - const href = action.getHref && action.getHref(actionContext); - - if (href) { - window.location.href = href; - return; - } - - await action.execute(actionContext); - } - - private async executeMultipleActions(actions: Action[], actionContext: C) { - const panel = await buildContextMenuForActions({ - actions, - actionContext, - closeMenu: () => session.close(), - }); - const session = openContextMenu([panel]); - } - - public readonly executeTriggerActions = async (triggerId: string, actionContext: C) => { - const actions = await this.getTriggerCompatibleActions!(triggerId, actionContext); - - if (!actions.length) { - throw new Error( - `No compatible actions found to execute for trigger [triggerId = ${triggerId}].` - ); - } - - if (actions.length === 1) { - await this.executeSingleAction(actions[0], actionContext); - return; - } - - await this.executeMultipleActions(actions, actionContext); + /** + * @deprecated + * + * Use `plugins.uiActions.getTrigger(triggerId).exec(params)` instead. + */ + public readonly executeTriggerActions = async ( + triggerId: T, + context: TriggerContext + ) => { + const trigger = this.getTrigger(triggerId); + await trigger.exec(context); }; /** diff --git a/src/plugins/ui_actions/public/triggers/index.ts b/src/plugins/ui_actions/public/triggers/index.ts index a34c6eda61ba0..1ae2a19c4001f 100644 --- a/src/plugins/ui_actions/public/triggers/index.ts +++ b/src/plugins/ui_actions/public/triggers/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export { Trigger } from './trigger'; +export * from './trigger'; +export * from './trigger_contract'; +export * from './trigger_internal'; diff --git a/src/plugins/ui_actions/public/triggers/trigger.ts b/src/plugins/ui_actions/public/triggers/trigger.ts index ba83f5619e250..2c019b09881d1 100644 --- a/src/plugins/ui_actions/public/triggers/trigger.ts +++ b/src/plugins/ui_actions/public/triggers/trigger.ts @@ -17,8 +17,35 @@ * under the License. */ -export interface Trigger { - id: string; +import { TriggerContextMapping, TriggerId } from '../types'; + +/** + * This is a convenience interface used to register a *trigger*. + * + * `Trigger` specifies a named anchor to which `Action` can be attached. When + * `Trigger` is being *called* it creates a `Context` object and passes it to + * the `execute` method of an `Action`. + * + * More than one action can be attached to a single trigger, in which case when + * trigger is *called* it first displays a context menu for user to pick a + * single action to execute. + */ +export interface Trigger { + /** + * Unique name of the trigger as identified in `ui_actions` plugin trigger + * registry, such as "SELECT_RANGE_TRIGGER" or "VALUE_CLICK_TRIGGER". + */ + id: ID; + + /** + * User friendly name of the trigger. + */ title?: string; + + /** + * A longer user friendly description of the trigger. + */ description?: string; } + +export type TriggerContext = T extends TriggerId ? TriggerContextMapping[T] : never; diff --git a/src/plugins/ui_actions/public/triggers/trigger_contract.ts b/src/plugins/ui_actions/public/triggers/trigger_contract.ts new file mode 100644 index 0000000000000..853b83dccabcc --- /dev/null +++ b/src/plugins/ui_actions/public/triggers/trigger_contract.ts @@ -0,0 +1,56 @@ +/* + * 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 { TriggerContext } from './trigger'; +import { TriggerInternal } from './trigger_internal'; +import { TriggerId } from '../types'; + +/** + * This is a public representation of a trigger that is provided to other plugins. + */ +export class TriggerContract { + /** + * Unique name of the trigger as identified in `ui_actions` plugin trigger + * registry, such as "SELECT_RANGE_TRIGGER" or "VALUE_CLICK_TRIGGER". + */ + public readonly id: T; + + /** + * User friendly name of the trigger. + */ + public readonly title?: string; + + /** + * A longer user friendly description of the trigger. + */ + public readonly description?: string; + + constructor(private readonly internal: TriggerInternal) { + this.id = this.internal.trigger.id; + this.title = this.internal.trigger.title; + this.description = this.internal.trigger.description; + } + + /** + * Use this method to execute action attached to this trigger. + */ + public readonly exec = async (context: TriggerContext) => { + await this.internal.execute(context); + }; +} diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts new file mode 100644 index 0000000000000..efcdc72ecad57 --- /dev/null +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -0,0 +1,76 @@ +/* + * 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 { TriggerContext, Trigger } from './trigger'; +import { TriggerContract } from './trigger_contract'; +import { UiActionsService } from '../service'; +import { Action } from '../actions'; +import { buildContextMenuForActions, openContextMenu } from '../context_menu'; +import { TriggerId } from '../types'; + +/** + * Internal representation of a trigger kept for consumption only internally + * within `ui_actions` plugin. + */ +export class TriggerInternal { + public readonly contract = new TriggerContract(this); + + constructor(public readonly service: UiActionsService, public readonly trigger: Trigger) {} + + public async execute(context: TriggerContext) { + const triggerId = this.trigger.id; + const actions = await this.service.getTriggerCompatibleActions!(triggerId, context); + + if (!actions.length) { + throw new Error( + `No compatible actions found to execute for trigger [triggerId = ${triggerId}].` + ); + } + + if (actions.length === 1) { + await this.executeSingleAction(actions[0], context); + return; + } + + await this.executeMultipleActions(actions, context); + } + + private async executeSingleAction(action: Action>, context: TriggerContext) { + const href = action.getHref && action.getHref(context); + + if (href) { + window.location.href = href; + return; + } + + await action.execute(context); + } + + private async executeMultipleActions( + actions: Array>>, + context: TriggerContext + ) { + const panel = await buildContextMenuForActions({ + actions, + actionContext: context, + closeMenu: () => session.close(), + }); + const session = openContextMenu([panel]); + } +} diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 9bd6ffdef2af3..8daa893eb4347 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -18,8 +18,14 @@ */ import { Action } from './actions/action'; -import { Trigger } from './triggers/trigger'; +import { TriggerInternal } from './triggers/trigger_internal'; -export type TriggerRegistry = Map; +export type TriggerRegistry = Map>; export type ActionRegistry = Map; export type TriggerToActionsRegistry = Map; + +export type TriggerId = string; + +export interface TriggerContextMapping { + [key: string]: object; +}