From 4b4f9fffdbb510c28411cf6a79a1571facfad95f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 1 Jul 2020 13:05:59 +0200 Subject: [PATCH] POC --- .../public/actions/apply_filter_action.ts | 16 +- src/plugins/data/public/plugin.ts | 88 ++++-- .../public/lib/panel/embeddable_panel.tsx | 6 +- .../build_eui_context_menu_panels.tsx | 20 +- src/plugins/ui_actions/public/index.ts | 8 +- src/plugins/ui_actions/public/plugin.ts | 1 + .../public/service/ui_actions_service.ts | 31 ++ .../public/triggers/trigger_internal.ts | 44 ++- src/plugins/ui_actions/public/types.ts | 11 +- .../dashboard_drilldowns_services.ts | 3 +- .../drilldown.tsx | 83 ++--- .../dashboard_to_dashboard_drilldown/types.ts | 10 - .../embeddable_to_url_drilldown/README.md | 3 - .../embeddable_to_url_drilldown.tsx | 286 ------------------ .../embeddable_to_url_drilldown/index.ts | 7 - .../public/drilldowns/index.ts | 7 - ...beddable_url_drilldown_context_provider.ts | 49 +++ ...able_url_drilldown_select_range_trigger.ts | 47 +++ ...dable_url_drilldown_value_click_trigger.ts | 44 +++ .../public/embeddables/index.ts | 3 + .../embeddable_enhanced/public/plugin.ts | 36 ++- .../action_wizard/action_wizard.tsx | 71 ++++- .../connected_flyout_manage_drilldowns.tsx | 25 +- .../flyout_drilldown_wizard.tsx | 29 +- .../form_drilldown_wizard.tsx | 12 + .../public/drilldowns/index.ts | 2 +- .../README.md | 0 .../components/index.ts | 0 .../url_drilldown_collect_config.tsx | 0 .../index.ts | 9 + .../types.ts | 10 + .../url_drilldown/url_drilldown.tsx | 143 +++++++++ ...url_drilldown_context_provider_registry.ts | 39 +++ .../url_drilldown_trigger_registry.ts | 37 +++ .../url_template.ts | 0 .../utils.ts | 0 .../ui_actions_enhanced/public/index.ts | 2 + .../ui_actions_enhanced/public/plugin.ts | 46 ++- .../ui_actions_service_enhancements.ts | 59 +++- .../ui_actions_enhanced/public/types.ts | 2 + 40 files changed, 787 insertions(+), 502 deletions(-) delete mode 100644 x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/README.md delete mode 100644 x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/embeddable_to_url_drilldown.tsx delete mode 100644 x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/index.ts delete mode 100644 x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_context_provider.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_select_range_trigger.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_value_click_trigger.ts rename x-pack/plugins/ui_actions_enhanced/public/drilldowns/{url_drilldown_lib => url_drilldown}/README.md (100%) rename x-pack/plugins/ui_actions_enhanced/public/drilldowns/{url_drilldown_lib => url_drilldown}/components/index.ts (100%) rename x-pack/plugins/ui_actions_enhanced/public/drilldowns/{url_drilldown_lib => url_drilldown}/components/url_drilldown_collect_config.tsx (100%) rename x-pack/plugins/ui_actions_enhanced/public/drilldowns/{url_drilldown_lib => url_drilldown}/index.ts (52%) rename x-pack/plugins/ui_actions_enhanced/public/drilldowns/{url_drilldown_lib => url_drilldown}/types.ts (57%) create mode 100644 x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx create mode 100644 x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_context_provider_registry.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_trigger_registry.ts rename x-pack/plugins/ui_actions_enhanced/public/drilldowns/{url_drilldown_lib => url_drilldown}/url_template.ts (100%) rename x-pack/plugins/ui_actions_enhanced/public/drilldowns/{url_drilldown_lib => url_drilldown}/utils.ts (100%) diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index 7e8ed5ec8fb22f..25f4670abf64d6 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -19,19 +19,19 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../kibana_react/public'; -import { ActionByType, createAction, IncompatibleActionError } from '../../../ui_actions/public'; +import { + ActionByType, + ApplyFilterTriggerContext, + createAction, + IncompatibleActionError, +} from '../../../ui_actions/public'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; import { Filter, FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_GLOBAL_APPLY_FILTER = 'ACTION_GLOBAL_APPLY_FILTER'; -export interface ApplyGlobalFilterActionContext { - filters: Filter[]; - timeFieldName?: string; -} - -async function isCompatible(context: ApplyGlobalFilterActionContext) { +async function isCompatible(context: ApplyFilterTriggerContext) { return context.filters !== undefined; } @@ -49,7 +49,7 @@ export function createFilterAction( }); }, isCompatible, - execute: async ({ filters, timeFieldName }: ApplyGlobalFilterActionContext) => { + execute: async ({ filters, timeFieldName }: ApplyFilterTriggerContext) => { if (!filters) { throw new Error('Applying a filter requires a filter'); } diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 493733daf261b2..411c69f28d96c6 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -20,14 +20,14 @@ import './index.scss'; import { - PluginInitializerContext, CoreSetup, CoreStart, - Plugin, PackageInfo, + Plugin, + PluginInitializerContext, } from 'src/core/public'; import { ConfigSchema } from '../config'; -import { Storage, IStorageWrapper, createStartServicesGetter } from '../../kibana_utils/public'; +import { createStartServicesGetter, IStorageWrapper, Storage } from '../../kibana_utils/public'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -55,31 +55,24 @@ import { import { createSearchBar } from './ui/search_bar/create_search_bar'; import { esaggs } from './search/expressions'; import { - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, + ApplyFilterTriggerContext, + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, } from '../../ui_actions/public'; import { ACTION_GLOBAL_APPLY_FILTER, createFilterAction, - createFiltersFromValueClickAction, createFiltersFromRangeSelectAction, + createFiltersFromValueClickAction, } from './actions'; -import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action'; -import { - selectRangeAction, - SelectRangeActionContext, - ACTION_SELECT_RANGE, -} from './actions/select_range_action'; -import { - valueClickAction, - ACTION_VALUE_CLICK, - ValueClickActionContext, -} from './actions/value_click_action'; +import { ACTION_SELECT_RANGE, SelectRangeActionContext } from './actions/select_range_action'; +import { ACTION_VALUE_CLICK, ValueClickActionContext } from './actions/value_click_action'; +import { ValueClickTriggerContext, RangeSelectTriggerContext } from '../../embeddable/public'; declare module '../../ui_actions/public' { export interface ActionContextMapping { - [ACTION_GLOBAL_APPLY_FILTER]: ApplyGlobalFilterActionContext; + [ACTION_GLOBAL_APPLY_FILTER]: ApplyFilterTriggerContext; [ACTION_SELECT_RANGE]: SelectRangeActionContext; [ACTION_VALUE_CLICK]: ValueClickActionContext; } @@ -126,19 +119,60 @@ export class DataPublicPlugin implements Plugin { + const filters = await createFiltersFromValueClickAction(context.data); + if (filters.length > 0) return true; + return false; + }, + originContextToDestContext: async ( + context: ValueClickTriggerContext + ): Promise => { + const filters = await createFiltersFromValueClickAction(context.data); + return { + embeddable: context.embeddable, + filters, + timeFieldName: context.data.timeFieldName, + }; + }, + }); + + uiActions.registerTriggerReaction({ + originTrigger: SELECT_RANGE_TRIGGER, + destTrigger: APPLY_FILTER_TRIGGER, + isCompatible: async (context: RangeSelectTriggerContext) => { + const filters = await createFiltersFromRangeSelectAction(context.data); + if (filters.length > 0) return true; + return false; + }, + originContextToDestContext: async ( + context: RangeSelectTriggerContext + ): Promise => { + const filters = await createFiltersFromRangeSelectAction(context.data); + return { + embeddable: context.embeddable, + filters, + timeFieldName: context.data.timeFieldName, + }; + }, + }); return { autocomplete: this.autocomplete.setup(core), diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 8cf2e015f88cf1..19599be232c040 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -311,8 +311,10 @@ export class EmbeddablePanel extends React.Component { const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); return await buildContextMenuForActions({ - actions: sortedActions, - actionContext: { embeddable: this.props.embeddable }, + actionsWithContext: sortedActions.map((action) => [ + action, + { embeddable: this.props.embeddable }, + ]), closeMenu: this.closeMyContextMenuPanel, }); }; 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..42fcc157ae7717 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 { ActionContextTuple } from '../triggers'; export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { defaultMessage: 'Options', @@ -32,19 +33,16 @@ export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { * Transforms an array of Actions to the shape EuiContextMenuPanel expects. */ export async function buildContextMenuForActions({ - actions, - actionContext, + actionsWithContext, title = defaultTitle, closeMenu, }: { - actions: Array>; - actionContext: Context; + actionsWithContext: ActionContextTuple[]; title?: string; closeMenu: () => void; }): Promise { const menuItems = await buildEuiContextMenuPanelItems({ - actions, - actionContext, + actionsWithContext, closeMenu, }); @@ -59,16 +57,14 @@ export async function buildContextMenuForActions({ * Transform an array of Actions into the shape needed to build an EUIContextMenu */ async function buildEuiContextMenuPanelItems({ - actions, - actionContext, + actionsWithContext, closeMenu, }: { - actions: Array>; - actionContext: Context; + actionsWithContext: ActionContextTuple[]; closeMenu: () => void; }) { - const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length); - const promises = actions.map(async (action, index) => { + const items: EuiContextMenuPanelItemDescriptor[] = new Array(actionsWithContext.length); + const promises = actionsWithContext.map(async ([action, actionContext], index) => { const isCompatible = await action.isCompatible(actionContext); if (!isCompatible) { return; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index a9b413fb36542d..364344a3446508 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -44,5 +44,11 @@ export { APPLY_FILTER_TRIGGER, applyFilterTrigger, } from './triggers'; -export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types'; +export { + TriggerContextMapping, + TriggerId, + ActionContextMapping, + ActionType, + ApplyFilterTriggerContext, +} from './types'; export { ActionByType } from './actions'; diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts index 71148656cbb164..17f534d231c532 100644 --- a/src/plugins/ui_actions/public/plugin.ts +++ b/src/plugins/ui_actions/public/plugin.ts @@ -29,6 +29,7 @@ export type UiActionsSetup = Pick< | 'registerAction' | 'registerTrigger' | 'unregisterAction' + | 'registerTriggerReaction' >; export type UiActionsStart = PublicMethodsOf; 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..52a40c13139141 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -29,6 +29,18 @@ import { Trigger, TriggerContext } from '../triggers/trigger'; import { TriggerInternal } from '../triggers/trigger_internal'; import { TriggerContract } from '../triggers/trigger_contract'; +export interface TriggerReaction< + OriginTrigger extends TriggerId = TriggerId, + DestTrigger extends TriggerId = TriggerId +> { + originTrigger: OriginTrigger; + destTrigger: DestTrigger; + originContextToDestContext( + context: TriggerContextMapping[OriginTrigger] + ): Promise; + isCompatible(context: TriggerContextMapping[OriginTrigger]): Promise; +} + export interface UiActionsServiceParams { readonly triggers?: TriggerRegistry; readonly actions?: ActionRegistry; @@ -43,6 +55,7 @@ export class UiActionsService { protected readonly triggers: TriggerRegistry; protected readonly actions: ActionRegistry; protected readonly triggerToActions: TriggerToActionsRegistry; + protected readonly triggerReactions = new Map(); constructor({ triggers = new Map(), @@ -63,8 +76,26 @@ export class UiActionsService { this.triggers.set(trigger.id, triggerInternal); this.triggerToActions.set(trigger.id, []); + this.triggerReactions.set(trigger.id, []); + }; + + public readonly registerTriggerReaction = (triggerReaction: TriggerReaction) => { + if (!this.triggers.has(triggerReaction.originTrigger)) { + throw new Error(`Trigger [trigger.id = ${triggerReaction.originTrigger}] not registered.`); + } + if (!this.triggers.has(triggerReaction.destTrigger)) { + throw new Error(`Trigger [trigger.id = ${triggerReaction.destTrigger}] not registered.`); + } + + this.triggerReactions.set(triggerReaction.originTrigger, [ + ...this.triggerReactions.get(triggerReaction.originTrigger)!, + triggerReaction, + ]); }; + public readonly getTriggerReactions = (triggerId: TriggerId) => + Array.from(this.triggerReactions.get(triggerId) ?? []); + public readonly getTrigger = (triggerId: T): TriggerContract => { const trigger = this.triggers.get(triggerId); diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index e499c404ae7457..2d53999abf6c30 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -22,7 +22,9 @@ import { TriggerContract } from './trigger_contract'; import { UiActionsService } from '../service'; import { Action } from '../actions'; import { buildContextMenuForActions, openContextMenu } from '../context_menu'; -import { TriggerId, TriggerContextMapping } from '../types'; +import { TriggerId, TriggerContextMapping, BaseContext } from '../types'; + +export type ActionContextTuple = [Action, BaseContext]; /** * Internal representation of a trigger kept for consumption only internally @@ -37,34 +39,46 @@ export class TriggerInternal { const triggerId = this.trigger.id; const actions = await this.service.getTriggerCompatibleActions!(triggerId, context); - if (!actions.length) { + const actionsWithContexts: ActionContextTuple[] = actions.map((action) => [action, context]); + + // TODO: make this recursive + const triggerReactions = this.service.getTriggerReactions(triggerId); + for (const reaction of triggerReactions) { + if (await reaction.isCompatible(context)) { + const reactionContext = await reaction.originContextToDestContext(context); + const reactionActions = await this.service.getTriggerCompatibleActions( + reaction.destTrigger, + reactionContext + ); + const reactionActionsWithContext = reactionActions.map((reactionAction) => [ + reactionAction, + reactionContext, + ]); + actionsWithContexts.push(...(reactionActionsWithContext as ActionContextTuple[])); + } + } + + if (!actionsWithContexts.length) { throw new Error( `No compatible actions found to execute for trigger [triggerId = ${triggerId}].` ); } - if (actions.length === 1) { - await this.executeSingleAction(actions[0], context); + if (actionsWithContexts.length === 1) { + await this.executeSingleAction(actionsWithContexts[0]); return; } - await this.executeMultipleActions(actions, context); + await this.executeMultipleActions(actionsWithContexts); } - private async executeSingleAction( - action: Action, - context: TriggerContextMapping[T] - ) { + private async executeSingleAction([action, context]: ActionContextTuple) { await action.execute(context); } - private async executeMultipleActions( - actions: Array>, - context: TriggerContextMapping[T] - ) { + private async executeMultipleActions(actionsWithContext: ActionContextTuple[]) { const panel = await buildContextMenuForActions({ - actions, - actionContext: context, + actionsWithContext, title: this.trigger.title, closeMenu: () => session.close(), }); diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 85c87306cc4f94..08300a03402f80 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -35,14 +35,17 @@ export type TriggerId = keyof TriggerContextMapping; export type BaseContext = object; export type TriggerContext = BaseContext; +export interface ApplyFilterTriggerContext { + embeddable?: IEmbeddable; + filters: Filter[]; + timeFieldName?: string; +} + export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; [SELECT_RANGE_TRIGGER]: RangeSelectTriggerContext; [VALUE_CLICK_TRIGGER]: ValueClickTriggerContext; - [APPLY_FILTER_TRIGGER]: { - embeddable: IEmbeddable; - filters: Filter[]; - }; + [APPLY_FILTER_TRIGGER]: ApplyFilterTriggerContext; } const DEFAULT_ACTION = ''; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts index 4325e3309b898a..5cc1dbe302aacc 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -16,6 +16,7 @@ import { } from './actions'; import { DashboardToDashboardDrilldown } from './dashboard_to_dashboard_drilldown'; import { createStartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; +import { APPLY_FILTER_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; declare module '../../../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -61,6 +62,6 @@ export class DashboardDrilldownsService { start, getDashboardUrlGenerator, }); - uiActions.registerDrilldown(dashboardToDashboardDrilldown); + uiActions.registerDrilldown(dashboardToDashboardDrilldown, [APPLY_FILTER_TRIGGER]); } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 26a69132cffb17..3ffa2998164cf9 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -7,27 +7,30 @@ import React from 'react'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { DashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public'; -import { ActionContext, Config } from './types'; +import { Config } from './types'; import { CollectConfigContainer } from './components'; import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; -import { esFilters } from '../../../../../../../src/plugins/data/public'; -import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; import { - isRangeSelectTriggerContext, - isValueClickTriggerContext, -} from '../../../../../../../src/plugins/embeddable/public'; + esFilters, + isFilters, + isQuery, + isTimeRange, +} from '../../../../../../../src/plugins/data/public'; + import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public'; import { StartDependencies } from '../../../plugin'; +import { ApplyFilterTriggerContext } from '../../../../../../../src/plugins/ui_actions/public'; export interface Params { start: StartServicesGetter>; getDashboardUrlGenerator: () => DashboardUrlGenerator; } -export class DashboardToDashboardDrilldown - implements Drilldown> { +export type ActionContext = ApplyFilterTriggerContext; + +export class DashboardToDashboardDrilldown implements Drilldown { constructor(protected readonly params: Params) {} public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN; @@ -55,17 +58,11 @@ export class DashboardToDashboardDrilldown return true; }; - public readonly getHref = async ( - config: Config, - context: ActionContext - ): Promise => { + public readonly getHref = async (config: Config, context: ActionContext): Promise => { return this.getDestinationUrl(config, context); }; - public readonly execute = async ( - config: Config, - context: ActionContext - ) => { + public readonly execute = async (config: Config, context: ActionContext) => { const dashboardPath = await this.getDestinationUrl(config, context); const dashboardHash = dashboardPath.split('#')[1]; @@ -74,19 +71,13 @@ export class DashboardToDashboardDrilldown }); }; - private getDestinationUrl = async ( - config: Config, - context: ActionContext - ): Promise => { - const { - createFiltersFromRangeSelectAction, - createFiltersFromValueClickAction, - } = this.params.start().plugins.data.actions; - const { - timeRange: currentTimeRange, - query, - filters: currentFilters, - } = context.embeddable!.getInput(); + private getDestinationUrl = async (config: Config, context: ActionContext): Promise => { + const embeddableInput = context.embeddable!.getInput(); + const currentFilters = isFilters(embeddableInput.filters) ? embeddableInput.filters : []; + const currentTimeRange = isTimeRange(embeddableInput.timeRange) + ? embeddableInput.timeRange + : undefined; + const currentQuery = isQuery(embeddableInput.query) ? embeddableInput.query : undefined; // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) // otherwise preserve only pinned @@ -99,37 +90,11 @@ export class DashboardToDashboardDrilldown // if undefined is passed, then destination dashboard will figure out time range itself // for brush event this time range would be overwritten let timeRange = config.useCurrentDateRange ? currentTimeRange : undefined; - let filtersFromEvent = await (async () => { - try { - if (isRangeSelectTriggerContext(context)) - return await createFiltersFromRangeSelectAction(context.data); - if (isValueClickTriggerContext(context)) - return await createFiltersFromValueClickAction(context.data); - - // eslint-disable-next-line no-console - console.warn( - ` - DashboardToDashboard drilldown: can't extract filters from action. - Is it not supported action?`, - context - ); - - return []; - } catch (e) { - // eslint-disable-next-line no-console - console.warn( - ` - DashboardToDashboard drilldown: error extracting filters from action. - Continuing without applying filters from event`, - e - ); - return []; - } - })(); - if (context.data.timeFieldName) { + let filtersFromEvent = context.filters; + if (context.timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.data.timeFieldName, + context.timeFieldName, filtersFromEvent ); filtersFromEvent = restOfFilters; @@ -140,7 +105,7 @@ export class DashboardToDashboardDrilldown return this.params.getDashboardUrlGenerator().createUrl({ dashboardId: config.dashboardId, - query: config.useCurrentFilters ? query : undefined, + query: config.useCurrentFilters ? currentQuery : undefined, timeRange, filters: [...existingFilters, ...filtersFromEvent], }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 1fbff0a7269e26..426e250499de02 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -4,16 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ValueClickTriggerContext, - RangeSelectTriggerContext, - IEmbeddable, -} from '../../../../../../../src/plugins/embeddable/public'; - -export type ActionContext = - | ValueClickTriggerContext - | RangeSelectTriggerContext; - export interface Config { dashboardId?: string; useCurrentFilters: boolean; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/README.md b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/README.md deleted file mode 100644 index 3d9c7c64d432fe..00000000000000 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Embeddable to URL drilldown - -Specific url drilldown implementation which relies on the `IEmbeddable` as context and on `ValueClickTrigger` and `RangeSelectTrigger` for action triggers diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/embeddable_to_url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/embeddable_to_url_drilldown.tsx deleted file mode 100644 index 9d472ada136061..00000000000000 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/embeddable_to_url_drilldown.tsx +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import _ from 'lodash'; -import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; -import { - UiActionsEnhancedDrilldownDefinition as DrilldownDefinition, - UiActionsEnhancedUrlDrilldownConfig as UrlDrilldownConfig, - UiActionsEnhancedUrlDrilldownScope as UrlDrilldownScope, - UiActionsEnhancedUrlDrilldownGlobalScope as UrlDrilldownGlobalScope, - UiActionsEnhancedUrlDrilldownCollectConfig as UrlDrilldownCollectConfig, - uiActionsEnhancedUrlDrilldownCompile as compile, -} from '../../../../ui_actions_enhanced/public'; -import { - IEmbeddable, - isRangeSelectTriggerContext, - isValueClickTriggerContext, - RangeSelectTriggerContext, - ValueClickTriggerContext, -} from '../../../../../../src/plugins/embeddable/public'; -import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; -import { - DataPublicPluginStart, - esFilters, - Filter, - Query, - TimeRange, -} from '../../../../../../src/plugins/data/public'; - -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; -export type CollectConfigProps = CollectConfigPropsBase< - UrlDrilldownConfig, - { embeddable?: IEmbeddable } ->; - -interface EmbeddableContextScope { - panelId?: string; - panelTitle?: string; - savedObjectId?: string; - filters?: Filter[]; - query?: Query; - timeRange?: TimeRange; -} - -type EmbeddableToUrlDrilldownScope = UrlDrilldownScope< - EmbeddableContextScope, - EmbeddableTriggerEventScope ->; - -interface EmbeddableTriggerEventScope { - /** - * More then one filter could come, for example, from heat map visualization - */ - filters: EmbeddableTriggerFilter[]; - /** - * 1st el from {@link filters}. just a shortcut. - */ - filter?: EmbeddableTriggerFilter; -} - -/** - * Generalized & simplified interface which covers possible filters - * that can be created from {@link ValueClickTriggerContext} & {@link RangeSelectTriggerContext} triggers - */ -interface EmbeddableTriggerFilter { - key: string; - value: string; - negate: boolean; - from: string; - to: string; -} - -const mockEventScope: EmbeddableTriggerEventScope = { - filter: { - key: '__testValueKey__', - value: '__testValueValue__', - from: '__testValueFrom__', - to: '__testValueTo__', - negate: false, - }, - filters: [ - { - key: '__testValueKey__', - value: '__testValueValue__', - from: '__testValueFrom__', - to: '__testValueTo__', - negate: false, - }, - ], -}; - -function buildScope( - global: UrlDrilldownGlobalScope, - context: EmbeddableContextScope, - event: EmbeddableTriggerEventScope = mockEventScope -): EmbeddableToUrlDrilldownScope { - return { - ...global, - context, - event, - }; -} - -type DataActionsHelpers = Pick< - DataPublicPluginStart['actions'], - 'createFiltersFromValueClickAction' | 'createFiltersFromRangeSelectAction' ->; -export interface Params { - /** - * Inject global static variables - */ - getGlobalScope: () => UrlDrilldownGlobalScope; - - /** - * Dependency on data plugin to extract filters from Click & Range actions - */ - getDataActionsHelpers: () => DataActionsHelpers; -} - -export class EmbeddableToUrlDrilldownDefinition - implements DrilldownDefinition { - public readonly id = 'EMB_TO_URL_DRILLDOWN'; - - public readonly minimalLicense = 'gold'; - - public readonly order = 8; - - public readonly getDisplayName = () => 'Go to URL'; - - public readonly euiIcon = 'link'; - - constructor(private params: Params) {} - - private readonly ReactCollectConfig: React.FC = ({ - config, - onConfig, - context, - }) => { - const { getGlobalScope } = this.params; - // eslint-disable-next-line react-hooks/rules-of-hooks - const scope = React.useMemo( - () => buildScope(getGlobalScope(), getContextScopeFromEmbeddable(context.embeddable)), - [getGlobalScope, context] - ); - - return ; - }; - - public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); - - public readonly createConfig = () => ({ - url: '', - openInNewTab: false, - }); - - public readonly isConfigValid = (config: UrlDrilldownConfig): config is UrlDrilldownConfig => { - if (!config.url) return false; - return isValidUrl(config.url); - }; - - /** - * `getHref` is need to support mouse middle-click and Cmd + Click behavior - * to open a link in new tab. - */ - public readonly getHref = async (config: UrlDrilldownConfig, context: ActionContext) => { - const globalScope = this.params.getGlobalScope(); - const contextScope = getContextScopeFromEmbeddable(context.embeddable); - const eventScope = await getEventScopeFromActionContext( - context, - this.params.getDataActionsHelpers() - ); - - const scope = buildScope(globalScope, contextScope, eventScope); - const url = compile(config.url, scope); - - return url; - }; - - public readonly execute = async (config: UrlDrilldownConfig, context: ActionContext) => { - const url = await this.getHref(config, context); - - if (config.openInNewTab) { - window.open(url, '_blank', 'noopener'); - } else { - window.location.href = url; - } - }; -} - -function getContextScopeFromEmbeddable(embeddable?: IEmbeddable): EmbeddableContextScope { - if (!embeddable) return {}; - const input = embeddable.getInput(); - const output = embeddable.getOutput(); - // TODO: type it better - return { - panelId: input.id, - panelTitle: output.title, - ..._.pick(input, ['query', 'timeRange', 'filters']), - ...(output.savedObjectId - ? { savedObjectId: output.savedObjectId } - : _.pick(input, 'savedObjectId')), - }; -} - -async function getEventScopeFromActionContext( - context: ActionContext, - { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction }: DataActionsHelpers -): Promise { - const filtersFromEvent = await (async () => { - try { - if (isRangeSelectTriggerContext(context)) - return await createFiltersFromRangeSelectAction(context.data); - if (isValueClickTriggerContext(context)) - return await createFiltersFromValueClickAction(context.data); - - // eslint-disable-next-line no-console - console.warn( - ` - Url drilldown: can't extract filters from action. - Is it not supported action?`, - context - ); - - return []; - } catch (e) { - // eslint-disable-next-line no-console - console.warn( - ` - URL drilldown: error extracting filters from action. - Continuing without applying filters from event`, - e - ); - return []; - } - })(); - - function dataFilterToEmbeddableTriggerFilter(filter: Filter): EmbeddableTriggerFilter { - if (esFilters.isRangeFilter(filter)) { - const rangeKey = Object.keys(filter.range)[0]; - const range = filter.range[rangeKey]; - return { - key: rangeKey ?? filter.meta.key ?? '', - value: (range.from ?? range.gt ?? range.gte ?? '').toString(), - from: (range.from ?? range.gt ?? range.gte ?? '').toString(), - to: (range.to ?? range.lt ?? range.lte ?? '').toString(), - negate: filter.meta.negate ?? false, - }; - } else { - const value = - (filter.meta.value && - (typeof filter.meta.value === 'string' ? filter.meta.value : filter.meta.value())) ?? - ''; - return { - key: filter.meta.key ?? '', - value: - (filter.meta.value && - (typeof filter.meta.value === 'string' ? filter.meta.value : filter.meta.value())) ?? - '', - from: value, - to: value, - negate: filter.meta.negate ?? false, - }; - } - } - - const eventFilters = filtersFromEvent.map(dataFilterToEmbeddableTriggerFilter); - const eventScope: EmbeddableTriggerEventScope = { - filters: eventFilters, - filter: eventFilters[0], - }; - - return eventScope; -} - -export function isValidUrl(url: string) { - try { - new URL(url); - return true; - } catch { - return false; - } -} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/index.ts deleted file mode 100644 index 966488da4cc6c6..00000000000000 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/embeddable_to_url_drilldown/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { EmbeddableToUrlDrilldownDefinition } from './embeddable_to_url_drilldown'; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts deleted file mode 100644 index 388a97c14cfda0..00000000000000 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './embeddable_to_url_drilldown'; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_context_provider.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_context_provider.ts new file mode 100644 index 00000000000000..6a2c1ecaaff500 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_context_provider.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UiActionsEnhancedUrlDrilldownContextProvider } from '../../../ui_actions_enhanced/public'; +import { Query, Filter, TimeRange } from '../../../../../src/plugins/data/public'; +import { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; + +function hasEmbeddable(val: unknown): val is { embeddable: IEmbeddable } { + if (val && typeof val === 'object' && 'embeddable' in val) return true; + return false; +} + +interface EmbeddableUrlDrilldownContext { + embeddableId: string; + embeddableTitle?: string; + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; + savedObjectId?: string; +} + +export const embeddableUrlDrilldownContextProvider: UiActionsEnhancedUrlDrilldownContextProvider = { + injectedKeys: [ + 'embeddableId', + 'embeddableTitle', + 'query', + 'timeRange', + 'filters', + 'savedObjectId', + ], + injectContext(executionContext: unknown) { + if (hasEmbeddable(executionContext)) { + const embeddable = executionContext.embeddable; + const input = embeddable.getInput(); + const output = embeddable.getOutput(); + return { + embeddableId: input.id, + embeddableTitle: output.title ?? input.title, + ..._.pick(input, ['query', 'timeRange', 'filters']), + ...(output.savedObjectId + ? { savedObjectId: output.savedObjectId } + : _.pick(input, 'savedObjectId')), + }; + } + }, +}; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_select_range_trigger.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_select_range_trigger.ts new file mode 100644 index 00000000000000..6ca0ed8bb58264 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_select_range_trigger.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UiActionsEnhancedUrlDrilldownTriggerDefinition } from '../../../ui_actions_enhanced/public'; +import { SELECT_RANGE_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; +import { + ChartActionContext, + isRangeSelectTriggerContext, + RangeSelectTriggerContext, +} from '../../../../../src/plugins/embeddable/public'; + +interface SelectRangeEventScope { + from: string; + to: string; + key: string; + negate: boolean; +} + +export const embeddableUrlDrilldownSelectRangeTrigger: UiActionsEnhancedUrlDrilldownTriggerDefinition< + typeof SELECT_RANGE_TRIGGER, + SelectRangeEventScope +> = { + triggerId: SELECT_RANGE_TRIGGER, + isTriggerContext(context: object): boolean { + return isRangeSelectTriggerContext((context as unknown) as ChartActionContext); + }, + getScopeForPreview(): SelectRangeEventScope { + return { + from: '__testFrom__', + to: '__testFrom__', + key: '__testKey__', + negate: false, + }; + }, + getScopeFromActionContext(context: RangeSelectTriggerContext): SelectRangeEventScope { + // TODO: map here to proper scope + return { + from: '__testFrom__', + to: '__testTo__', + key: '__testKey__', + negate: false, + }; + }, +}; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_value_click_trigger.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_value_click_trigger.ts new file mode 100644 index 00000000000000..1c32e43c379e79 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_url_drilldown_value_click_trigger.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UiActionsEnhancedUrlDrilldownTriggerDefinition } from '../../../ui_actions_enhanced/public'; +import { VALUE_CLICK_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; +import { + ChartActionContext, + isValueClickTriggerContext, + ValueClickTriggerContext, +} from '../../../../../src/plugins/embeddable/public'; + +interface ValueClickEventScope { + value: string; + key: string; + negate: boolean; +} + +export const embeddableUrlDrilldownValueClickTrigger: UiActionsEnhancedUrlDrilldownTriggerDefinition< + typeof VALUE_CLICK_TRIGGER, + ValueClickEventScope +> = { + triggerId: VALUE_CLICK_TRIGGER, + isTriggerContext(context: object): boolean { + return isValueClickTriggerContext((context as unknown) as ChartActionContext); + }, + getScopeForPreview(): ValueClickEventScope { + return { + value: '__testValue__', + key: '__testKey__', + negate: false, + }; + }, + getScopeFromActionContext(context: ValueClickTriggerContext): ValueClickEventScope { + // TODO: map here to proper scope + return { + value: '__testValue__', + key: '__testKey__', + negate: false, + }; + }, +}; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/index.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/index.ts index fabbc60a13f67a..97fee388e5df22 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/index.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/index.ts @@ -6,3 +6,6 @@ export * from './is_enhanced_embeddable'; export * from './embeddable_action_storage'; +export * from './embeddable_url_drilldown_context_provider'; +export * from './embeddable_url_drilldown_select_range_trigger'; +export * from './embeddable_url_drilldown_value_click_trigger'; diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index 4c114445eab3fa..35f9d9aae64660 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { SavedObjectAttributes } from 'kibana/public'; import { + defaultEmbeddableFactoryProvider, + EmbeddableContext, EmbeddableFactory, EmbeddableFactoryDefinition, EmbeddableInput, @@ -14,8 +16,6 @@ import { EmbeddableSetup, EmbeddableStart, IEmbeddable, - defaultEmbeddableFactoryProvider, - EmbeddableContext, PANEL_NOTIFICATION_TRIGGER, ViewMode, } from '../../../../src/plugins/embeddable/public'; @@ -25,14 +25,17 @@ import { EmbeddableWithDynamicActions, } from './embeddables/embeddable_action_storage'; import { - UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, + UiActionsEnhancedDynamicActionManager as DynamicActionManager, } from '../../ui_actions_enhanced/public'; -import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; -import { EmbeddableToUrlDrilldownDefinition } from './drilldowns'; -import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; +import { ACTION_PANEL_NOTIFICATIONS, PanelNotificationsAction } from './actions'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { + embeddableUrlDrilldownContextProvider, + embeddableUrlDrilldownSelectRangeTrigger, + embeddableUrlDrilldownValueClickTrigger, +} from './embeddables'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -67,21 +70,16 @@ export class EmbeddableEnhancedPlugin public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); - const start = createStartServicesGetter(core.getStartServices); - const getDataActionsHelpers = () => { - return start().plugins.data.actions; - }; - const panelNotificationAction = new PanelNotificationsAction(); plugins.uiActionsEnhanced.registerAction(panelNotificationAction); plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); - plugins.uiActionsEnhanced.registerDrilldown( - new EmbeddableToUrlDrilldownDefinition({ - getGlobalScope: () => ({ - kibanaUrl: window.location.origin + core.http.basePath.get(), - }), - getDataActionsHelpers, - }) + + plugins.uiActionsEnhanced.urlDrilldown.registerContextProvider( + embeddableUrlDrilldownContextProvider + ); + plugins.uiActionsEnhanced.urlDrilldown.registerTrigger(embeddableUrlDrilldownValueClickTrigger); + plugins.uiActionsEnhanced.urlDrilldown.registerTrigger( + embeddableUrlDrilldownSelectRangeTrigger ); return {}; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index 6769c8bab07327..07a98bd479b11f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -19,6 +19,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { txtChangeButton } from './i18n'; import './action_wizard.scss'; import { ActionFactory } from '../../dynamic_actions'; +import { + SELECT_RANGE_TRIGGER, + TriggerId, + VALUE_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; export interface ActionWizardProps { /** @@ -52,6 +57,11 @@ export interface ActionWizardProps { * Context will be passed into ActionFactory's methods */ context: object; + + selectedTrigger?: TriggerId; + onSelectedTriggerChange: (triggerId?: TriggerId) => void; + + getTriggersForActionFactory: (actionFactoryId: string) => TriggerId[]; } export const ActionWizard: React.FC = ({ @@ -61,6 +71,9 @@ export const ActionWizard: React.FC = ({ onConfigChange, config, context, + selectedTrigger, + onSelectedTriggerChange, + getTriggersForActionFactory, }) => { // auto pick action factory if there is only 1 available if ( @@ -71,7 +84,24 @@ export const ActionWizard: React.FC = ({ onActionFactoryChange(actionFactories[0]); } + // auto pick selected trigger if there is only 1 available + if (currentActionFactory && !selectedTrigger) { + const triggers = getTriggersForActionFactory(currentActionFactory.id); + if (triggers.length === 1) { + onSelectedTriggerChange(triggers[0]); + } + } + if (currentActionFactory && config) { + if (!selectedTrigger) { + return ( + + ); + } + return ( = ({ onDeselect={() => { onActionFactoryChange(undefined); }} - context={context} + context={{ ...context, selectedTrigger }} config={config} onConfigChange={(newConfig) => { onConfigChange(newConfig); @@ -224,3 +254,42 @@ const ActionFactorySelector: React.FC = ({ ); }; + +// TODO: move it out of hre +const triggerToName: { [key: string]: string } = { + [VALUE_CLICK_TRIGGER]: 'Value click', + [SELECT_RANGE_TRIGGER]: 'Range select', +}; + +const TriggerPicker: React.FC<{ + triggers: TriggerId[]; + onTriggerSelected: (trigger: TriggerId) => void; +}> = ({ triggers, onTriggerSelected }) => { + if (triggers.length === 0) { + // this is not user facing, as it would be impossible to get into this state + // just leaving for dev purposes for troubleshooting + return
No triggers to pick from
; + } + + // The below style is applied to fix Firefox rendering bug. + // See: https://github.com/elastic/kibana/pull/61219/#pullrequestreview-402903330 + const firefoxBugFix = { + willChange: 'opacity', + }; + + return ( + + {triggers.map((trigger) => ( + + onTriggerSelected(trigger)} + > + <> + + + ))} + + ); +}; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 2e7f734a170fa2..43a69e344a8b56 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -11,9 +11,8 @@ import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldow import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import { - VALUE_CLICK_TRIGGER, - SELECT_RANGE_TRIGGER, TriggerContextMapping, + TriggerId, } from '../../../../../../../src/plugins/ui_actions/public'; import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public'; import { DrilldownListItem } from '../list_manage_drilldowns'; @@ -55,11 +54,13 @@ enum Routes { export function createFlyoutManageDrilldowns({ actionFactories: allActionFactories, + getTriggersForActionFactory, storage, toastService, docsLink, }: { actionFactories: ActionFactory[]; + getTriggersForActionFactory: (actionFactoryId: string) => TriggerId[]; storage: IStorageWrapper; toastService: ToastsStart; docsLink?: string; @@ -72,18 +73,7 @@ export function createFlyoutManageDrilldowns({ return (props: ConnectedFlyoutManageDrilldownsProps) => { const isCreateOnly = props.viewMode === 'create'; - const selectedTriggers: Array = React.useMemo( - () => [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER], - [] - ); - - const factoryContext: object = React.useMemo( - () => ({ - ...props.context, - triggers: selectedTriggers, - }), - [props.context, selectedTriggers] - ); + const factoryContext: object = props.context ?? {}; const actionFactories = useCompatibleActionFactoriesForCurrentContext( allActionFactories, @@ -161,7 +151,7 @@ export function createFlyoutManageDrilldowns({ onClose={props.onClose} mode={route === Routes.Create ? 'create' : 'edit'} onBack={isCreateOnly ? undefined : () => setRoute(Routes.Manage)} - onSubmit={({ actionConfig, actionFactory, name }) => { + onSubmit={({ actionConfig, actionFactory, name, selectedTrigger }) => { if (route === Routes.Create) { createDrilldown( { @@ -169,7 +159,7 @@ export function createFlyoutManageDrilldowns({ config: actionConfig, factoryId: actionFactory.id, }, - selectedTriggers + [selectedTrigger] ); } else { editDrilldown( @@ -179,7 +169,7 @@ export function createFlyoutManageDrilldowns({ config: actionConfig, factoryId: actionFactory.id, }, - selectedTriggers + [selectedTrigger] ); } @@ -200,6 +190,7 @@ export function createFlyoutManageDrilldowns({ }} actionFactoryContext={factoryContext} initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()} + getTriggersForActionFactory={getTriggersForActionFactory} /> ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index 58cf2501280c72..9cfeadc2cf0f89 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -17,15 +17,21 @@ import { } from './i18n'; import { DrilldownHelloBar } from '../drilldown_hello_bar'; import { ActionFactory } from '../../../dynamic_actions'; +import { TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; -export interface DrilldownWizardConfig { +export interface DrilldownWizardConfig< + ActionConfig extends object = object, + Trigger extends TriggerId = TriggerId +> { name: string; actionFactory?: ActionFactory; actionConfig?: ActionConfig; + selectedTrigger?: Trigger; } export interface FlyoutDrilldownWizardProps { drilldownActionFactories: ActionFactory[]; + getTriggersForActionFactory: (actionFactoryId: string) => TriggerId[]; onSubmit?: (drilldownWizardConfig: Required) => void; onDelete?: () => void; @@ -51,6 +57,7 @@ function useWizardConfigState( setName: (name: string) => void; setActionConfig: (actionConfig: object) => void; setActionFactory: (actionFactory?: ActionFactory) => void; + setSelectedTrigger: (triggerId?: TriggerId) => void; } ] { const [wizardConfig, setWizardConfig] = useState( @@ -89,6 +96,7 @@ function useWizardConfigState( ...wizardConfig, actionFactory, actionConfig: actionConfigCache[actionFactory.id] ?? actionFactory.createConfig(), + selectedTrigger: undefined, }); } else { if (wizardConfig.actionFactory?.id) { @@ -102,9 +110,16 @@ function useWizardConfigState( ...wizardConfig, actionFactory: undefined, actionConfig: undefined, + selectedTrigger: undefined, }); } }, + setSelectedTrigger: (triggerId?: TriggerId) => { + setWizardConfig({ + ...wizardConfig, + selectedTrigger: triggerId, + }); + }, }, ]; } @@ -121,10 +136,12 @@ export function FlyoutDrilldownWizard) { - const [wizardConfig, { setActionFactory, setActionConfig, setName }] = useWizardConfigState( - initialDrilldownWizardConfig - ); + const [ + wizardConfig, + { setActionFactory, setActionConfig, setName, setSelectedTrigger }, + ] = useWizardConfigState(initialDrilldownWizardConfig); const isActionValid = ( config: DrilldownWizardConfig @@ -132,6 +149,7 @@ export function FlyoutDrilldownWizard {mode === 'edit' && ( <> diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx index e7e7f72dbf58f4..9abdfb675baeb3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n'; import { ActionFactory } from '../../../dynamic_actions'; import { ActionWizard } from '../../../components/action_wizard'; +import { TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions'; @@ -27,6 +28,11 @@ export interface FormDrilldownWizardProps { onActionConfigChange?: (config: object) => void; actionFactories?: ActionFactory[]; + + selectedTrigger?: TriggerId; + onSelectedTriggerChange?: (triggerId?: TriggerId) => void; + + getTriggersForActionFactory: (actionFactoryId: string) => TriggerId[]; } export const FormDrilldownWizard: React.FC = ({ @@ -38,6 +44,9 @@ export const FormDrilldownWizard: React.FC = ({ onActionFactoryChange = noopFn, actionFactories = [], actionFactoryContext, + selectedTrigger, + onSelectedTriggerChange = noopFn, + getTriggersForActionFactory, }) => { const nameFragment = ( @@ -86,6 +95,9 @@ export const FormDrilldownWizard: React.FC = ({ onActionFactoryChange={(actionFactory) => onActionFactoryChange(actionFactory)} onConfigChange={(config) => onActionConfigChange(config)} context={actionFactoryContext} + getTriggersForActionFactory={getTriggersForActionFactory} + selectedTrigger={selectedTrigger} + onSelectedTriggerChange={onSelectedTriggerChange} /> ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts index e15381ad1b33d3..525712ce5a86fb 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts @@ -6,4 +6,4 @@ export * from './drilldown_definition'; export * from './components'; -export * from './url_drilldown_lib'; +export * from './url_drilldown'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/README.md b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/README.md rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config.tsx similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/components/url_drilldown_collect_config.tsx rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config.tsx diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/index.ts rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts index e98f4d5a7ded9f..8e815e05ecb652 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts @@ -7,3 +7,12 @@ export * from './types'; export * from './components'; export { compile } from './url_template'; +export { UrlDrilldownDefinition } from './url_drilldown'; +export { + UrlDrilldownTriggerRegistry, + UrlDrilldownTriggerDefinition, +} from './url_drilldown_trigger_registry'; +export { + UrlDrilldownContextProviderRegistry, + UrlDrilldownContextProvider, +} from './url_drilldown_context_provider_registry'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts similarity index 57% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/types.ts rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts index 039b02b0abd7b0..0a00a2e450f646 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UrlDrilldownTriggerDefinition } from './url_drilldown_trigger_registry'; +import { UrlDrilldownContextProvider } from './url_drilldown_context_provider_registry'; + export interface UrlDrilldownConfig { url: string; openInNewTab: boolean; @@ -20,3 +23,10 @@ export interface UrlDrilldownScope< export interface UrlDrilldownGlobalScope { kibanaUrl: string; } + +export interface UrlDrilldownService { + registerTrigger(urlDrilldownTrigger: UrlDrilldownTriggerDefinition): void; + registerContextProvider( + urlDrilldownContextProvider: UrlDrilldownContextProvider + ): void; +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx new file mode 100644 index 00000000000000..be64e84849b469 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; +import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; +import { TriggerId } from '../../../../../../src/plugins/ui_actions/public'; +import { UrlDrilldownConfig, UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; +import { DrilldownDefinition } from '../drilldown_definition'; +import { UrlDrilldownTriggerRegistry } from './url_drilldown_trigger_registry'; +import { UrlDrilldownCollectConfig } from './components'; +import { compile } from './url_template'; +import { UrlDrilldownContextProviderRegistry } from './url_drilldown_context_provider_registry'; + +export type CollectConfigProps = CollectConfigPropsBase< + UrlDrilldownConfig, + { + selectedTrigger?: TriggerId; + } +>; + +type ActionContext = object; + +function buildScope( + global: UrlDrilldownGlobalScope, + context: UrlDrilldownScope['context'], + event: UrlDrilldownScope['event'] +): UrlDrilldownScope { + return { + ...global, + context, + event, + }; +} + +export interface Params { + /** + * Inject global static variables + */ + getGlobalScope: () => UrlDrilldownGlobalScope; + + /** + * Lets url drilldown know about trigger type + */ + triggerRegistry: UrlDrilldownTriggerRegistry; + + /** + * Lets url know about context (e.g. embeddable context) + */ + contextProviderRegistry: UrlDrilldownContextProviderRegistry; +} + +export class UrlDrilldownDefinition + implements DrilldownDefinition { + public readonly id = 'URL_DRILLDOWN'; + + public readonly minimalLicense = 'gold'; + + public readonly order = 8; + + public readonly getDisplayName = () => 'Go to URL'; + + public readonly euiIcon = 'link'; + + private readonly triggerRegistry: UrlDrilldownTriggerRegistry; + private readonly contextProviderRegistry: UrlDrilldownContextProviderRegistry; + + constructor(private params: Params) { + this.triggerRegistry = params.triggerRegistry; + this.contextProviderRegistry = params.contextProviderRegistry; + } + + private readonly ReactCollectConfig: React.FC = ({ + config, + onConfig, + context, + }) => { + const { getGlobalScope } = this.params; + // eslint-disable-next-line react-hooks/rules-of-hooks + const scope = React.useMemo( + () => + buildScope( + getGlobalScope(), // global + this.contextProviderRegistry.buildContextScope(context), // embeddable + context.selectedTrigger + ? this.triggerRegistry.getEventScopeForPreview(context.selectedTrigger) // trigger + : {} + ), + [getGlobalScope, context] + ); + + return ; + }; + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + url: '', + openInNewTab: false, + }); + + public readonly isConfigValid = (config: UrlDrilldownConfig): config is UrlDrilldownConfig => { + if (!config.url) return false; + return isValidUrl(config.url); + }; + + /** + * `getHref` is need to support mouse middle-click and Cmd + Click behavior + * to open a link in new tab. + */ + public readonly getHref = async (config: UrlDrilldownConfig, context: ActionContext) => { + const globalScope = this.params.getGlobalScope(); + const contextScope = this.contextProviderRegistry.buildContextScope(context); + const eventScope = await this.triggerRegistry.getEventScopeFromActionContext(context); + + const scope = buildScope(globalScope, contextScope, eventScope); + const url = compile(config.url, scope); + + return url; + }; + + public readonly execute = async (config: UrlDrilldownConfig, context: ActionContext) => { + const url = await this.getHref(config, context); + + if (config.openInNewTab) { + window.open(url, '_blank', 'noopener'); + } else { + window.location.href = url; + } + }; +} + +export function isValidUrl(url: string) { + try { + new URL(url); + return true; + } catch { + return false; + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_context_provider_registry.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_context_provider_registry.ts new file mode 100644 index 00000000000000..15e6cb89b18a24 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_context_provider_registry.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// TODO: this is very lossy typed +// not sure if we can improve this +// ActionContext or CollectConfigContext +type ExecutionContext = unknown; + +export interface UrlDrilldownContextProvider { + injectedKeys: Array; + injectedKeysMeta?: Record; + injectContext(executionContext: ExecutionContext): InjectedContext | undefined; +} + +export class UrlDrilldownContextProviderRegistry { + private registry: UrlDrilldownContextProvider[] = []; + private reservedKeys = new Set(); + registerContextDefinition(provider: UrlDrilldownContextProvider) { + const hasConflicts = provider.injectedKeys.some((key) => this.reservedKeys.has(key)); + if (hasConflicts) { + throw new Error('Put better error message here'); + } + + this.registry.push(provider); + provider.injectedKeys.forEach((key) => this.reservedKeys.add(key)); + } + + buildContextScope(context: ExecutionContext): object { + return this.registry.reduce((res, provider) => { + return { + ...res, + ...provider.injectContext(context), + }; + }, {}); + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_trigger_registry.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_trigger_registry.ts new file mode 100644 index 00000000000000..8029a78d458c51 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_trigger_registry.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TriggerContextMapping, TriggerId } from '../../../../../../src/plugins/ui_actions/public'; + +export interface UrlDrilldownTriggerDefinition< + T extends TriggerId = TriggerId, + EventScope extends object = {} +> { + triggerId: T; + isTriggerContext(context: object): boolean; + getScopeForPreview(): EventScope; + getScopeFromActionContext(context: TriggerContextMapping[T]): EventScope; + scopeMeta?: Record; +} + +export class UrlDrilldownTriggerRegistry { + private registry = new Map(); + registerTriggerDefinition(def: UrlDrilldownTriggerDefinition) { + this.registry.set(def.triggerId, def); + } + + getEventScopeForPreview(triggerId: TriggerId) { + return this.registry.get(triggerId)?.getScopeForPreview() ?? {}; + } + + getEventScopeFromActionContext(context: TriggerContextMapping[T]) { + const matchingTrigger = Array.from(this.registry.values()).find((triggerDescriptor) => + triggerDescriptor.isTriggerContext(context) + ); + if (!matchingTrigger) return {}; + return matchingTrigger.getScopeFromActionContext(context); + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/url_template.ts rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/utils.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/utils.ts similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown_lib/utils.ts rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/utils.ts diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts index 68b56b7f3ccdb2..9d2a8275604ad6 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts @@ -37,4 +37,6 @@ export { UrlDrilldownGlobalScope as UiActionsEnhancedUrlDrilldownGlobalScope, UrlDrilldownScope as UiActionsEnhancedUrlDrilldownScope, compile as uiActionsEnhancedUrlDrilldownCompile, + UrlDrilldownContextProvider as UiActionsEnhancedUrlDrilldownContextProvider, + UrlDrilldownTriggerDefinition as UiActionsEnhancedUrlDrilldownTriggerDefinition, } from './drilldowns'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index a625ea2e2118bc..8b1f213587fb03 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -33,8 +33,16 @@ import { import { CommonlyUsedRange } from './types'; import { UiActionsServiceEnhancements } from './services'; import { ILicense, LicensingPluginStart } from '../../licensing/public'; -import { createFlyoutManageDrilldowns } from './drilldowns'; +import { + createFlyoutManageDrilldowns, + UrlDrilldownContextProviderRegistry, + UrlDrilldownDefinition, + UrlDrilldownTriggerRegistry, + UrlDrilldownService, +} from './drilldowns'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { UrlDrilldownTriggerDefinition } from './drilldowns/url_drilldown/url_drilldown_trigger_registry'; +import { UrlDrilldownContextProvider } from './drilldowns/url_drilldown/url_drilldown_context_provider_registry'; interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. @@ -49,7 +57,12 @@ interface StartDependencies { export interface SetupContract extends UiActionsSetup, - Pick {} + Pick< + UiActionsServiceEnhancements, + 'registerDrilldown' | 'addTriggerActionFactory' | 'getTriggersForActionFactory' + > { + urlDrilldown: UrlDrilldownService; +} export interface StartContract extends UiActionsStart, @@ -83,9 +96,37 @@ export class AdvancedUiActionsPublicPlugin constructor(initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { uiActions }: SetupDependencies): SetupContract { + const urlDrilldownTriggerRegistry = new UrlDrilldownTriggerRegistry(); + const urlDrilldownContextProviderRegistry = new UrlDrilldownContextProviderRegistry(); + const urlDrilldownDefinition = new UrlDrilldownDefinition({ + getGlobalScope: () => ({ + kibanaUrl: window.location.origin + core.http.basePath.get(), + }), + triggerRegistry: urlDrilldownTriggerRegistry, + contextProviderRegistry: urlDrilldownContextProviderRegistry, + }); + + this.enhancements.registerDrilldown(urlDrilldownDefinition); + return { ...uiActions, ...this.enhancements, + urlDrilldown: { + registerTrigger: (urlDrilldownTrigger: UrlDrilldownTriggerDefinition) => { + urlDrilldownTriggerRegistry.registerTriggerDefinition(urlDrilldownTrigger); + this.enhancements.addTriggerActionFactory( + urlDrilldownTrigger.triggerId, + this.enhancements.getActionFactory(urlDrilldownDefinition.id) + ); + }, + registerContextProvider( + urlDrilldownContextProvider: UrlDrilldownContextProvider + ) { + urlDrilldownContextProviderRegistry.registerContextDefinition( + urlDrilldownContextProvider as UrlDrilldownContextProvider + ); + }, + }, }; } @@ -116,6 +157,7 @@ export class AdvancedUiActionsPublicPlugin ...this.enhancements, FlyoutManageDrilldowns: createFlyoutManageDrilldowns({ actionFactories: this.enhancements.getActionFactories(), + getTriggersForActionFactory: this.enhancements.getTriggersForActionFactory, storage: new Storage(window?.localStorage), toastService: core.notifications.toasts, docsLink: core.docLinks.links.dashboard.drilldowns, 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..8a2425e088a7df 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 @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionFactoryRegistry } from '../types'; +import { ActionFactoriesToTriggersRegistry, ActionFactoryRegistry } from '../types'; import { ActionFactory, ActionFactoryDefinition } from '../dynamic_actions'; import { DrilldownDefinition } from '../drilldowns'; import { ILicense } from '../../../licensing/common/types'; +import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public'; export interface UiActionsServiceEnhancementsParams { readonly actionFactories?: ActionFactoryRegistry; @@ -16,11 +17,13 @@ export interface UiActionsServiceEnhancementsParams { export class UiActionsServiceEnhancements { protected readonly actionFactories: ActionFactoryRegistry; + protected readonly actionFactoriesToTriggers: ActionFactoriesToTriggersRegistry; protected readonly getLicenseInfo: () => ILicense; constructor({ actionFactories = new Map(), getLicenseInfo }: UiActionsServiceEnhancementsParams) { this.actionFactories = actionFactories; this.getLicenseInfo = getLicenseInfo; + this.actionFactoriesToTriggers = new Map(); } /** @@ -44,6 +47,7 @@ export class UiActionsServiceEnhancements { ); this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory); + this.actionFactoriesToTriggers.set(actionFactory.id, []); }; public readonly getActionFactory = (actionFactoryId: string): ActionFactory => { @@ -68,20 +72,24 @@ export class UiActionsServiceEnhancements { */ public readonly registerDrilldown = < Config extends object = object, - ExecutionContext extends object = object - >({ - id: factoryId, - order, - CollectConfig, - createConfig, - isConfigValid, - getDisplayName, - euiIcon, - execute, - getHref, - minimalLicense, - }: DrilldownDefinition): void => { - const actionFactory: ActionFactoryDefinition = { + ExecutionContext extends object = object, + T extends TriggerId = TriggerId + >( + { + id: factoryId, + order, + CollectConfig, + createConfig, + isConfigValid, + getDisplayName, + euiIcon, + execute, + getHref, + minimalLicense, + }: DrilldownDefinition, + supportedTriggers: T[] = [] + ): void => { + const actionFactory: ActionFactoryDefinition = { id: factoryId, minimalLicense, order, @@ -99,8 +107,27 @@ export class UiActionsServiceEnhancements { execute: async (context) => await execute(serializedAction.config, context), getHref: getHref ? async (context) => getHref(serializedAction.config, context) : undefined, }), - } as ActionFactoryDefinition; + } as ActionFactoryDefinition; this.registerActionFactory(actionFactory); + supportedTriggers.forEach((trigger) => + this.addTriggerActionFactory(trigger, this.getActionFactory(actionFactory.id)) + ); + }; + + public readonly addTriggerActionFactory = ( + triggerId: T, + actionFactory: ActionFactory + ): void => { + if (!this.actionFactoriesToTriggers.has(actionFactory.id)) { + throw new Error(`ActionFactory [actionFactory.id = ${actionFactory.id}] is not registered`); + } + this.actionFactoriesToTriggers.set(actionFactory.id, [ + ...this.actionFactoriesToTriggers.get(actionFactory.id)!.filter((t) => t !== triggerId), + triggerId, + ]); }; + + public readonly getTriggersForActionFactory = (actionFactoryId: string) => + Array.from(this.actionFactoriesToTriggers.get(actionFactoryId) ?? []); } diff --git a/x-pack/plugins/ui_actions_enhanced/public/types.ts b/x-pack/plugins/ui_actions_enhanced/public/types.ts index 5c960192dcaff9..fe69e9653ba69c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/types.ts @@ -6,6 +6,7 @@ import { KibanaReactOverlays } from '../../../../src/plugins/kibana_react/public'; import { ActionFactory } from './dynamic_actions'; +import { TriggerId } from '../../../../src/plugins/ui_actions/public'; export interface CommonlyUsedRange { from: string; @@ -16,3 +17,4 @@ export interface CommonlyUsedRange { export type OpenModal = KibanaReactOverlays['openModal']; export type ActionFactoryRegistry = Map; +export type ActionFactoriesToTriggersRegistry = Map;