[UiActions] pass trigger into action execution context (#74363) (#75029)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Anton Dosov 2020-08-14 15:28:44 +02:00 committed by GitHub
parent 603292ca11
commit 455b33bf87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 277 additions and 68 deletions

View file

@ -21,7 +21,11 @@ import { OverlayStart } from 'kibana/public';
import { EuiFieldText, EuiModalBody, EuiButton } from '@elastic/eui';
import { useState } from 'react';
import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
import { createAction, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import {
ActionExecutionContext,
createAction,
UiActionsStart,
} from '../../../../src/plugins/ui_actions/public';
export const USER_TRIGGER = 'USER_TRIGGER';
export const COUNTRY_TRIGGER = 'COUNTRY_TRIGGER';
@ -37,7 +41,8 @@ export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY';
export const showcasePluggability = createAction<typeof ACTION_SHOWCASE_PLUGGABILITY>({
type: ACTION_SHOWCASE_PLUGGABILITY,
getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.',
execute: async () => alert("Isn't that cool?!"),
execute: async (context: ActionExecutionContext) =>
alert(`Isn't that cool?! Triggered by ${context.trigger?.id} trigger`),
});
export interface PhoneContext {

View file

@ -97,9 +97,9 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => {
});
uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction);
setConfirmationText(
`You've successfully added a new action: ${dynamicAction.getDisplayName(
{}
)}. Refresh the page to reset state. It's up to the user of the system to persist state like this.`
`You've successfully added a new action: ${dynamicAction.getDisplayName({
trigger: uiActionsApi.getTrigger(HELLO_WORLD_TRIGGER_ID),
})}. Refresh the page to reset state. It's up to the user of the system to persist state like this.`
);
}}
>

View file

@ -19,6 +19,7 @@
import { createFilterAction } from './apply_filter_action';
import { expectErrorAsync } from '../../tests/helpers';
import { defaultTrigger } from '../../../../ui_actions/public/triggers';
test('has ACTION_APPLY_FILTER type and id', () => {
const action = createFilterAction();
@ -51,6 +52,7 @@ describe('isCompatible()', () => {
}),
} as any,
filters: [],
trigger: defaultTrigger,
});
expect(result).toBe(true);
});
@ -66,6 +68,7 @@ describe('isCompatible()', () => {
}),
} as any,
filters: [],
trigger: defaultTrigger,
});
expect(result).toBe(false);
});
@ -119,6 +122,7 @@ describe('execute()', () => {
await action.execute({
embeddable,
filters: ['FILTER' as any],
trigger: defaultTrigger,
});
expect(root.updateInput).toHaveBeenCalledTimes(1);

View file

@ -30,6 +30,7 @@ import {
PANEL_BADGE_TRIGGER,
PANEL_NOTIFICATION_TRIGGER,
EmbeddableContext,
contextMenuTrigger,
} from '../triggers';
import { IEmbeddable, EmbeddableOutput, EmbeddableError } from '../embeddables/i_embeddable';
import { ViewMode } from '../types';
@ -311,7 +312,11 @@ export class EmbeddablePanel extends React.Component<Props, State> {
const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField);
return await buildContextMenuForActions({
actions: sortedActions.map((action) => [action, { embeddable: this.props.embeddable }]),
actions: sortedActions.map((action) => ({
action,
context: { embeddable: this.props.embeddable },
trigger: contextMenuTrigger,
})),
closeMenu: this.closeMyContextMenuPanel,
});
};

View file

@ -31,6 +31,7 @@ import { ContactCardEmbeddable } from '../../../../test_samples';
import { esFilters, Filter } from '../../../../../../../../plugins/data/public';
import { EmbeddableStart } from '../../../../../plugin';
import { embeddablePluginMock } from '../../../../../mocks';
import { defaultTrigger } from '../../../../../../../ui_actions/public/triggers';
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
@ -85,7 +86,9 @@ test('Is not compatible when container is in view mode', async () => {
() => null
);
container.updateInput({ viewMode: ViewMode.VIEW });
expect(await addPanelAction.isCompatible({ embeddable: container })).toBe(false);
expect(
await addPanelAction.isCompatible({ embeddable: container, trigger: defaultTrigger })
).toBe(false);
});
test('Is not compatible when embeddable is not a container', async () => {
@ -94,7 +97,7 @@ test('Is not compatible when embeddable is not a container', async () => {
test('Is compatible when embeddable is a parent and in edit mode', async () => {
container.updateInput({ viewMode: ViewMode.EDIT });
expect(await action.isCompatible({ embeddable: container })).toBe(true);
expect(await action.isCompatible({ embeddable: container, trigger: defaultTrigger })).toBe(true);
});
test('Execute throws an error when called with an embeddable that is not a container', async () => {
@ -108,6 +111,7 @@ test('Execute throws an error when called with an embeddable that is not a conta
},
{} as any
),
trigger: defaultTrigger,
} as any);
}
await expect(check()).rejects.toThrow(Error);
@ -116,6 +120,7 @@ test('Execute does not throw an error when called with a compatible container',
container.updateInput({ viewMode: ViewMode.EDIT });
await action.execute({
embeddable: container,
trigger: defaultTrigger,
});
});

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Action } from 'src/plugins/ui_actions/public';
import { Action, ActionExecutionContext } from 'src/plugins/ui_actions/public';
import { NotificationsStart, OverlayStart } from 'src/core/public';
import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
import { ViewMode } from '../../../../types';
@ -52,12 +52,14 @@ export class AddPanelAction implements Action<ActionContext> {
return 'plusInCircleFilled';
}
public async isCompatible({ embeddable }: ActionContext) {
public async isCompatible(context: ActionExecutionContext<ActionContext>) {
const { embeddable } = context;
return embeddable.getIsContainer() && embeddable.getInput().viewMode === ViewMode.EDIT;
}
public async execute({ embeddable }: ActionContext) {
if (!embeddable.getIsContainer() || !(await this.isCompatible({ embeddable }))) {
public async execute(context: ActionExecutionContext<ActionContext>) {
const { embeddable } = context;
if (!embeddable.getIsContainer() || !(await this.isCompatible(context))) {
throw new Error('Context is incompatible');
}

View file

@ -30,7 +30,7 @@ import React from 'react';
import { Action } from 'src/plugins/ui_actions/public';
import { PanelOptionsMenu } from './panel_options_menu';
import { IEmbeddable } from '../../embeddables';
import { EmbeddableContext } from '../../triggers';
import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers';
export interface PanelHeaderProps {
title?: string;
@ -49,11 +49,11 @@ function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmb
<EuiBadge
key={badge.id}
className="embPanel__headerBadge"
iconType={badge.getIconType({ embeddable })}
onClick={() => badge.execute({ embeddable })}
onClickAriaLabel={badge.getDisplayName({ embeddable })}
iconType={badge.getIconType({ embeddable, trigger: panelBadgeTrigger })}
onClick={() => badge.execute({ embeddable, trigger: panelBadgeTrigger })}
onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
>
{badge.getDisplayName({ embeddable })}
{badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
</EuiBadge>
));
}
@ -70,14 +70,17 @@ function renderNotifications(
data-test-subj={`embeddablePanelNotification-${notification.id}`}
key={notification.id}
style={{ marginTop: '4px', marginRight: '4px' }}
onClick={() => notification.execute(context)}
onClick={() => notification.execute({ ...context, trigger: panelNotificationTrigger })}
>
{notification.getDisplayName(context)}
{notification.getDisplayName({ ...context, trigger: panelNotificationTrigger })}
</EuiNotificationBadge>
);
if (notification.getDisplayNameTooltip) {
const tooltip = notification.getDisplayNameTooltip(context);
const tooltip = notification.getDisplayNameTooltip({
...context,
trigger: panelNotificationTrigger,
});
if (tooltip) {
badge = (

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { ActionByType, IncompatibleActionError, ActionType } from '../../ui_actions';
import { IncompatibleActionError, ActionType, ActionDefinitionByType } from '../../ui_actions';
import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables';
// Casting to ActionType is a hack - in a real situation use
@ -42,7 +42,7 @@ export interface SayHelloActionContext {
message?: string;
}
export class SayHelloAction implements ActionByType<typeof SAY_HELLO_ACTION> {
export class SayHelloAction implements ActionDefinitionByType<typeof SAY_HELLO_ACTION> {
public readonly type = SAY_HELLO_ACTION;
public readonly id = SAY_HELLO_ACTION;

View file

@ -31,6 +31,7 @@ import {
FilterableEmbeddableInput,
} from '../lib/test_samples';
import { esFilters } from '../../../data/public';
import { applyFilterTrigger } from '../../../ui_actions/public';
test('ApplyFilterAction applies the filter to the root of the container tree', async () => {
const { doStart, setup } = testPlugin();
@ -85,7 +86,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
query: { match: { extension: { query: 'foo' } } },
};
await applyFilterAction.execute({ embeddable, filters: [filter] });
await applyFilterAction.execute({ embeddable, filters: [filter], trigger: applyFilterTrigger });
expect(root.getInput().filters.length).toBe(1);
expect(node1.getInput().filters.length).toBe(1);
expect(embeddable.getInput().filters.length).toBe(1);

View file

@ -17,8 +17,9 @@
* under the License.
*/
import { createAction } from '../../../ui_actions/public';
import { ActionExecutionContext, createAction } from '../../../ui_actions/public';
import { ActionType } from '../types';
import { defaultTrigger } from '../triggers';
const sayHelloAction = createAction({
// Casting to ActionType is a hack - in a real situation use
@ -29,11 +30,17 @@ const sayHelloAction = createAction({
});
test('action is not compatible based on context', async () => {
const isCompatible = await sayHelloAction.isCompatible({ amICompatible: false });
const isCompatible = await sayHelloAction.isCompatible({
amICompatible: false,
trigger: defaultTrigger,
} as ActionExecutionContext);
expect(isCompatible).toBe(false);
});
test('action is compatible based on context', async () => {
const isCompatible = await sayHelloAction.isCompatible({ amICompatible: true });
const isCompatible = await sayHelloAction.isCompatible({
amICompatible: true,
trigger: defaultTrigger,
} as ActionExecutionContext);
expect(isCompatible).toBe(true);
});

View file

@ -18,13 +18,43 @@
*/
import { UiComponent } from 'src/plugins/kibana_utils/public';
import { ActionType, ActionContextMapping } from '../types';
import { ActionType, ActionContextMapping, BaseContext } from '../types';
import { Presentable } from '../util/presentable';
import { Trigger } from '../triggers';
export type ActionByType<T extends ActionType> = Action<ActionContextMapping[T], T>;
export type ActionDefinitionByType<T extends ActionType> = ActionDefinition<
ActionContextMapping[T]
>;
export interface Action<Context extends {} = {}, T = ActionType>
extends Partial<Presentable<Context>> {
/**
* During action execution we can provide additional information,
* for example, trigger, that caused the action execution
*/
export interface ActionExecutionMeta {
/**
* Trigger that executed the action
*/
trigger: Trigger;
}
/**
* Action methods are executed with Context from trigger + {@link ActionExecutionMeta}
*/
export type ActionExecutionContext<Context extends BaseContext = BaseContext> = Context &
ActionExecutionMeta;
/**
* Simplified action context for {@link ActionDefinition}
* When defining action consumer may use either it's own Context
* or an ActionExecutionContext<Context> to get access to {@link ActionExecutionMeta} params
*/
export type ActionDefinitionContext<Context extends BaseContext = BaseContext> =
| Context
| ActionExecutionContext<Context>;
export interface Action<Context extends BaseContext = {}, T = ActionType>
extends Partial<Presentable<ActionExecutionContext<Context>>> {
/**
* Determined the order when there is more than one action matched to a trigger.
* Higher numbers are displayed first.
@ -44,44 +74,51 @@ export interface Action<Context extends {} = {}, T = ActionType>
/**
* Optional EUI icon type that can be displayed along with the title.
*/
getIconType(context: Context): string | undefined;
getIconType(context: ActionExecutionContext<Context>): string | undefined;
/**
* Returns a title to be displayed to the user.
* @param context
*/
getDisplayName(context: Context): string;
getDisplayName(context: ActionExecutionContext<Context>): string;
/**
* `UiComponent` to render when displaying this action as a context menu item.
* If not provided, `getDisplayName` will be used instead.
*/
MenuItem?: UiComponent<{ context: Context }>;
MenuItem?: UiComponent<{ context: ActionExecutionContext<Context> }>;
/**
* Returns a promise that resolves to true if this action is compatible given the context,
* otherwise resolves to false.
*/
isCompatible(context: Context): Promise<boolean>;
isCompatible(context: ActionExecutionContext<Context>): Promise<boolean>;
/**
* Executes the action.
*/
execute(context: Context): Promise<void>;
execute(context: ActionExecutionContext<Context>): Promise<void>;
/**
* This method should return a link if this item can be clicked on. The link
* is used to navigate user if user middle-clicks it or Ctrl + clicks or
* right-clicks and selects "Open in new tab".
*/
getHref?(context: ActionExecutionContext<Context>): Promise<string | undefined>;
/**
* Determines if action should be executed automatically,
* without first showing up in context menu.
* false by default.
*/
shouldAutoExecute?(context: Context): Promise<boolean>;
shouldAutoExecute?(context: ActionExecutionContext<Context>): Promise<boolean>;
}
/**
* A convenience interface used to register an action.
*/
export interface ActionDefinition<Context extends object = object>
extends Partial<Presentable<Context>> {
export interface ActionDefinition<Context extends BaseContext = {}>
extends Partial<Presentable<ActionDefinitionContext<Context>>> {
/**
* ID of the action that uniquely identifies this action in the actions registry.
*/
@ -92,17 +129,30 @@ export interface ActionDefinition<Context extends object = object>
*/
readonly type?: ActionType;
/**
* Returns a promise that resolves to true if this item is compatible given
* the context and should be displayed to user, otherwise resolves to false.
*/
isCompatible?(context: ActionDefinitionContext<Context>): Promise<boolean>;
/**
* Executes the action.
*/
execute(context: Context): Promise<void>;
execute(context: ActionDefinitionContext<Context>): Promise<void>;
/**
* Determines if action should be executed automatically,
* without first showing up in context menu.
* false by default.
*/
shouldAutoExecute?(context: Context): Promise<boolean>;
shouldAutoExecute?(context: ActionDefinitionContext<Context>): Promise<boolean>;
/**
* This method should return a link if this item can be clicked on. The link
* is used to navigate user if user middle-clicks it or Ctrl + clicks or
* right-clicks and selects "Open in new tab".
*/
getHref?(context: ActionDefinitionContext<Context>): Promise<string | undefined>;
}
export type ActionContext<A> = A extends ActionDefinition<infer Context> ? Context : never;

View file

@ -23,13 +23,22 @@ import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { uiToReactComponent } from '../../../kibana_react/public';
import { Action } from '../actions';
import { Trigger } from '../triggers';
import { BaseContext } from '../types';
export const defaultTitle = i18n.translate('uiActions.actionPanel.title', {
defaultMessage: 'Options',
});
type ActionWithContext<Context extends BaseContext = BaseContext> = [Action<Context>, Context];
interface ActionWithContext<Context extends BaseContext = BaseContext> {
action: Action<Context>;
context: Context;
/**
* Trigger that caused this action
*/
trigger: Trigger;
}
/**
* Transforms an array of Actions to the shape EuiContextMenuPanel expects.
@ -66,15 +75,19 @@ async function buildEuiContextMenuPanelItems({
closeMenu: () => void;
}) {
const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length);
const promises = actions.map(async ([action, actionContext], index) => {
const isCompatible = await action.isCompatible(actionContext);
const promises = actions.map(async ({ action, context, trigger }, index) => {
const isCompatible = await action.isCompatible({
...context,
trigger,
});
if (!isCompatible) {
return;
}
items[index] = await convertPanelActionToContextMenuItem({
action,
actionContext,
actionContext: context,
trigger,
closeMenu,
});
});
@ -87,19 +100,30 @@ async function buildEuiContextMenuPanelItems({
async function convertPanelActionToContextMenuItem<Context extends object>({
action,
actionContext,
trigger,
closeMenu,
}: {
action: Action<Context>;
actionContext: Context;
trigger: Trigger;
closeMenu: () => void;
}): Promise<EuiContextMenuPanelItemDescriptor> {
const menuPanelItem: EuiContextMenuPanelItemDescriptor = {
name: action.MenuItem
? React.createElement(uiToReactComponent(action.MenuItem), {
context: actionContext,
context: {
...actionContext,
trigger,
},
})
: action.getDisplayName(actionContext),
icon: action.getIconType(actionContext),
: action.getDisplayName({
...actionContext,
trigger,
}),
icon: action.getIconType({
...actionContext,
trigger,
}),
panel: _.get(action, 'childContextMenuPanel.id'),
'data-test-subj': `embeddablePanelAction-${action.id}`,
};
@ -114,20 +138,29 @@ async function convertPanelActionToContextMenuItem<Context extends object>({
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys
) {
event.preventDefault();
action.execute(actionContext);
action.execute({
...actionContext,
trigger,
});
} else {
// let browser handle navigation
}
} else {
// not a link
action.execute(actionContext);
action.execute({
...actionContext,
trigger,
});
}
closeMenu();
};
if (action.getHref) {
const href = await action.getHref(actionContext);
const href = await action.getHref({
...actionContext,
trigger,
});
if (href) {
menuPanelItem.href = href;
}

View file

@ -45,4 +45,9 @@ export {
applyFilterTrigger,
} from './triggers';
export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types';
export { ActionByType } from './actions';
export {
ActionByType,
ActionDefinitionByType,
ActionExecutionContext,
ActionExecutionMeta,
} from './actions';

View file

@ -46,7 +46,7 @@ export class UiActionsExecutionService {
context: BaseContext;
trigger: Trigger;
}): Promise<void> {
const shouldBatch = !(await action.shouldAutoExecute?.(context)) ?? false;
const shouldBatch = !(await action.shouldAutoExecute?.({ ...context, trigger })) ?? false;
const task: ExecuteActionTask = {
action,
context,
@ -59,7 +59,7 @@ export class UiActionsExecutionService {
} else {
this.pendingTasks.add(task);
try {
await action.execute(context);
await action.execute({ ...context, trigger });
this.pendingTasks.delete(task);
} catch (e) {
this.pendingTasks.delete(task);
@ -96,9 +96,12 @@ export class UiActionsExecutionService {
}, 0);
}
private async executeSingleTask({ context, action, defer }: ExecuteActionTask) {
private async executeSingleTask({ context, action, defer, trigger }: ExecuteActionTask) {
try {
await action.execute(context);
await action.execute({
...context,
trigger,
});
defer.resolve();
} catch (e) {
defer.reject(e);
@ -107,7 +110,11 @@ export class UiActionsExecutionService {
private async executeMultipleActions(tasks: ExecuteActionTask[]) {
const panel = await buildContextMenuForActions({
actions: tasks.map(({ action, context }) => [action, context]),
actions: tasks.map(({ action, context, trigger }) => ({
action,
context,
trigger,
})),
title: tasks[0].trigger.title, // title of context menu is title of trigger which originated the chain
closeMenu: () => {
tasks.forEach((t) => t.defer.resolve());

View file

@ -142,7 +142,7 @@ export class UiActionsService {
triggerId: T,
// The action can accept partial or no context, but if it needs context not provided
// by this type of trigger, typescript will complain. yay!
action: Action<TriggerContextMapping[T]>
action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T]> // TODO: remove `Action` https://github.com/elastic/kibana/issues/74501
): void => {
if (!this.actions.has(action.id)) this.registerAction(action);
this.attachAction(triggerId, action.id);
@ -178,7 +178,14 @@ export class UiActionsService {
context: TriggerContextMapping[T]
): Promise<Array<Action<TriggerContextMapping[T]>>> => {
const actions = this.getTriggerActions!(triggerId);
const isCompatibles = await Promise.all(actions.map((action) => action.isCompatible(context)));
const isCompatibles = await Promise.all(
actions.map((action) =>
action.isCompatible({
...context,
trigger: this.getTrigger(triggerId),
})
)
);
return actions.reduce(
(acc: Array<Action<TriggerContextMapping[T]>>, action, i) =>
isCompatibles[i] ? [...acc, action] : acc,

View file

@ -82,7 +82,7 @@ test('executes a single action mapped to a trigger', async () => {
jest.runAllTimers();
expect(executeFn).toBeCalledTimes(1);
expect(executeFn).toBeCalledWith(context);
expect(executeFn).toBeCalledWith(expect.objectContaining(context));
});
test('throws an error if there are no compatible actions to execute', async () => {
@ -202,3 +202,25 @@ test("doesn't show a context menu for auto executable actions", async () => {
expect(openContextMenu).toHaveBeenCalledTimes(0);
});
});
test('passes trigger into execute', async () => {
const { setup, doStart } = uiActions;
const trigger = {
id: 'MY-TRIGGER' as TriggerId,
title: 'My trigger',
};
const action = createTestAction<{ foo: string }>('test', () => true);
setup.registerTrigger(trigger);
setup.addTriggerAction(trigger.id, action);
const start = doStart();
const context = { foo: 'bar' };
await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context);
jest.runAllTimers();
expect(executeFn).toBeCalledWith({
...context,
trigger,
});
});

View file

@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Trigger } from '.';
export const DEFAULT_TRIGGER = '';
export const defaultTrigger: Trigger<''> = {
id: DEFAULT_TRIGGER,
title: 'Unknown',
description: 'Unknown trigger.',
};

View file

@ -23,3 +23,4 @@ export * from './trigger_internal';
export * from './select_range_trigger';
export * from './value_click_trigger';
export * from './apply_filter_trigger';
export * from './default_trigger';

View file

@ -19,7 +19,12 @@
import { ActionInternal } from './actions/action_internal';
import { TriggerInternal } from './triggers/trigger_internal';
import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
APPLY_FILTER_TRIGGER,
DEFAULT_TRIGGER,
} from './triggers';
import type { RangeSelectContext, ValueClickContext } from '../../embeddable/public';
import type { ApplyGlobalFilterActionContext } from '../../data/public';
@ -27,8 +32,6 @@ export type TriggerRegistry = Map<TriggerId, TriggerInternal<any>>;
export type ActionRegistry = Map<string, ActionInternal>;
export type TriggerToActionsRegistry = Map<TriggerId, string[]>;
const DEFAULT_TRIGGER = '';
export type TriggerId = keyof TriggerContextMapping;
export type BaseContext = object;

View file

@ -10,6 +10,7 @@ import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/publ
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
import { ChartActionContext } from '../../../../../src/plugins/embeddable/public';
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public';
import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
function isValidUrl(url: string) {
try {
@ -101,7 +102,15 @@ export class DashboardToUrlDrilldown implements Drilldown<Config, ActionContext>
return config.url;
};
public readonly execute = async (config: Config, context: ActionContext) => {
public readonly execute = async (
config: Config,
context: ActionExecutionContext<ActionContext>
) => {
// Just for showcasing:
// we can get trigger a which caused this drilldown execution
// eslint-disable-next-line no-console
console.log(context.trigger?.id);
const url = await this.getHref(config, context);
if (config.openInNewTab) {

View file

@ -9,7 +9,10 @@ import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import moment from 'moment-timezone';
import { CoreSetup } from 'src/core/public';
import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public';
import {
UiActionsActionDefinition as ActionDefinition,
IncompatibleActionError,
} from '../../../../../src/plugins/ui_actions/public';
import { LicensingPluginSetup } from '../../../licensing/public';
import { checkLicense } from '../lib/license_check';
@ -31,7 +34,7 @@ interface ActionContext {
embeddable: ISearchEmbeddable;
}
export class GetCsvReportPanelAction implements Action<ActionContext> {
export class GetCsvReportPanelAction implements ActionDefinition<ActionContext> {
private isDownloading: boolean;
public readonly type = '';
public readonly id = CSV_REPORTING_ACTION;

View file

@ -7,7 +7,10 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public';
import { ActionByType, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public';
import {
ActionDefinitionByType,
IncompatibleActionError,
} from '../../../../src/plugins/ui_actions/public';
import { TimeRange } from '../../../../src/plugins/data/public';
import { CustomizeTimeRangeModal } from './customize_time_range_modal';
import { OpenModal, CommonlyUsedRange } from './types';
@ -38,7 +41,7 @@ export interface TimeRangeActionContext {
embeddable: Embeddable<TimeRangeInput>;
}
export class CustomTimeRangeAction implements ActionByType<typeof CUSTOM_TIME_RANGE> {
export class CustomTimeRangeAction implements ActionDefinitionByType<typeof CUSTOM_TIME_RANGE> {
public readonly type = CUSTOM_TIME_RANGE;
private openModal: OpenModal;
private dateFormat?: string;

View file

@ -6,6 +6,7 @@
import { ActionFactoryDefinition } from '../dynamic_actions';
import { LicenseType } from '../../../licensing/public';
import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
/**
* This is a convenience interface to register a drilldown. Drilldown has
@ -93,10 +94,16 @@ export interface DrilldownDefinition<
* @param context Object that represents context in which the underlying
* `UIAction` of this drilldown is being executed in.
*/
execute(config: Config, context: ExecutionContext): void;
execute(
config: Config,
context: ExecutionContext | ActionExecutionContext<ExecutionContext>
): void;
/**
* A link where drilldown should navigate on middle click or Ctrl + click.
*/
getHref?(config: Config, context: ExecutionContext): Promise<string | undefined>;
getHref?(
config: Config,
context: ExecutionContext | ActionExecutionContext<ExecutionContext>
): Promise<string | undefined>;
}