From 223a9d3a70fe1e158b77b7f8bc2f6bf7b31d12b1 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Wed, 26 Feb 2020 18:42:25 -0500 Subject: [PATCH] wip --- examples/ui_action_examples/public/plugin.ts | 8 ++- .../public/actions/actions.tsx | 17 ++--- examples/ui_actions_explorer/public/app.tsx | 9 +-- .../ui_actions_explorer/public/plugin.tsx | 46 ++++++------ .../public/actions/select_range_action.ts | 12 ++-- .../data/public/actions/value_click_action.ts | 12 ++-- src/legacy/core_plugins/data/public/plugin.ts | 28 ++++++-- .../public/actions/expand_panel_action.tsx | 14 ++-- .../public/actions/replace_panel_action.tsx | 14 ++-- .../public/plugin.tsx | 13 +++- .../public/tests/dashboard_container.test.tsx | 2 +- .../public/actions/apply_filter_action.ts | 12 ++-- src/plugins/data/public/plugin.ts | 9 ++- src/plugins/embeddable/public/bootstrap.ts | 16 +++++ .../public/lib/actions/apply_filter_action.ts | 10 +-- .../lib/panel/embeddable_panel.test.tsx | 10 +-- .../embeddable/public/lib/panel/index.ts | 4 +- .../public/lib/panel/panel_header/index.ts | 7 +- .../customize_title/customize_panel_action.ts | 2 +- .../panel_actions/customize_title/index.ts} | 12 +--- .../panel/panel_header/panel_actions/index.ts | 7 +- .../test_samples/actions/edit_mode_action.ts | 11 +-- .../test_samples/actions/say_hello_action.tsx | 12 ++-- .../actions/send_message_action.tsx | 8 +-- .../ui_actions/public/actions/action.test.ts | 18 +++-- .../ui_actions/public/actions/action.ts | 13 +++- .../public/actions/action_definition.ts | 72 +++++++++++++++++++ .../public/actions/create_action.ts | 8 +-- src/plugins/ui_actions/public/index.ts | 3 +- src/plugins/ui_actions/public/mocks.ts | 1 + .../public/service/ui_actions_service.test.ts | 57 ++++++++------- .../public/service/ui_actions_service.ts | 40 ++++++++--- .../tests/execute_trigger_actions.test.ts | 40 ++++++----- .../public/tests/get_trigger_actions.test.ts | 10 +-- .../get_trigger_compatible_actions.test.ts | 33 +++++---- .../tests/test_samples/hello_world_action.tsx | 11 +-- .../public/tests/test_samples/index.ts | 2 - .../tests/test_samples/say_hello_action.tsx | 46 ------------ src/plugins/ui_actions/public/types.ts | 11 ++- .../public/np_ready/public/plugin.tsx | 2 +- .../public/sample_panel_action.tsx | 14 ++-- .../public/sample_panel_link.ts | 10 +-- .../panel_actions/get_csv_panel_action.tsx | 21 ++++-- .../public/custom_time_range_action.tsx | 12 ++-- .../public/custom_time_range_badge.tsx | 14 ++-- .../advanced_ui_actions/public/plugin.ts | 23 ++++-- .../actions/flyout_create_drilldown/index.tsx | 4 +- x-pack/plugins/drilldowns/public/plugin.ts | 7 ++ .../public/service/drilldown_service.ts | 3 +- 49 files changed, 461 insertions(+), 309 deletions(-) rename src/plugins/{ui_actions/public/tests/test_samples/restricted_action.ts => embeddable/public/lib/panel/panel_header/panel_actions/customize_title/index.ts} (69%) create mode 100644 src/plugins/ui_actions/public/actions/action_definition.ts delete mode 100644 src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx diff --git a/examples/ui_action_examples/public/plugin.ts b/examples/ui_action_examples/public/plugin.ts index 08b65714dbf6..a406ca1229fd 100644 --- a/examples/ui_action_examples/public/plugin.ts +++ b/examples/ui_action_examples/public/plugin.ts @@ -19,7 +19,7 @@ import { Plugin, CoreSetup } from '../../../src/core/public'; import { UiActionsSetup } from '../../../src/plugins/ui_actions/public'; -import { createHelloWorldAction } from './hello_world_action'; +import { createHelloWorldAction, HELLO_WORLD_ACTION_TYPE } from './hello_world_action'; import { helloWorldTrigger, HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; interface UiActionExamplesSetupDependencies { @@ -30,6 +30,10 @@ declare module '../../../src/plugins/ui_actions/public' { export interface TriggerContextMapping { [HELLO_WORLD_TRIGGER_ID]: undefined; } + + export interface ActionContextMapping { + [HELLO_WORLD_ACTION_TYPE]: undefined; + } } export class UiActionExamplesPlugin @@ -42,7 +46,7 @@ export class UiActionExamplesPlugin })); uiActions.registerAction(helloWorldAction); - uiActions.attachAction(helloWorldTrigger.id, helloWorldAction.id); + uiActions.attachAction(helloWorldTrigger.id, helloWorldAction); } public start() {} diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index 2770b0e3bd5f..63605bb946d3 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -34,7 +34,7 @@ export const EDIT_USER_ACTION = 'EDIT_USER_ACTION'; export const PHONE_USER_ACTION = 'PHONE_USER_ACTION'; export const SHOWCASE_PLUGGABILITY_ACTION = 'SHOWCASE_PLUGGABILITY_ACTION'; -export const showcasePluggability = createAction({ +export const showcasePluggability = createAction({ type: SHOWCASE_PLUGGABILITY_ACTION, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', execute: async () => alert("Isn't that cool?!"), @@ -42,24 +42,24 @@ export const showcasePluggability = createAction({ export type PhoneContext = string; -export const makePhoneCallAction = createAction({ +export const makePhoneCallAction = createAction({ type: CALL_PHONE_NUMBER_ACTION, getDisplayName: () => 'Call phone number', execute: async phone => alert(`Pretend calling ${phone}...`), }); -export const lookUpWeatherAction = createAction<{ country: string }>({ +export const lookUpWeatherAction = createAction({ type: TRAVEL_GUIDE_ACTION, getIconType: () => 'popout', getDisplayName: () => 'View travel guide', - execute: async ({ country }) => { + execute: async country => { window.open(`https://www.worldtravelguide.net/?s=${country},`, '_blank'); }, }); export type CountryContext = string; -export const viewInMapsAction = createAction({ +export const viewInMapsAction = createAction({ type: VIEW_IN_MAPS_ACTION, getIconType: () => 'popout', getDisplayName: () => 'View in maps', @@ -100,10 +100,7 @@ function EditUserModal({ } export const createEditUserAction = (getOpenModal: () => Promise) => - createAction<{ - user: User; - update: (user: User) => void; - }>({ + createAction({ type: EDIT_USER_ACTION, getIconType: () => 'pencil', getDisplayName: () => 'Edit user', @@ -120,7 +117,7 @@ export interface UserContext { } export const createPhoneUserAction = (getUiActionsApi: () => Promise) => - createAction({ + createAction({ type: PHONE_USER_ACTION, getDisplayName: () => 'Call phone number', isCompatible: async ({ user }) => user.phone !== undefined, diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index e93e29ac444f..7919b645cfc4 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -76,8 +76,9 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { { - const dynamicAction = createAction<{}>({ - type: `${HELLO_WORLD_ACTION_TYPE}-${name}`, + const dynamicAction = createAction({ + id: `${HELLO_WORLD_ACTION_TYPE}-${name}`, + type: HELLO_WORLD_ACTION_TYPE, getDisplayName: () => `Say hello to ${name}`, execute: async () => { const overlay = openModal( @@ -95,10 +96,10 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { }, }); uiActionsApi.registerAction(dynamicAction); - uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction.type); + uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); setConfirmationText( `You've successfully added a new action: ${dynamicAction.getDisplayName( - {} + undefined )}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` ); }} diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index fecada71099e..536eedcca014 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -27,17 +27,17 @@ import { lookUpWeatherAction, viewInMapsAction, createEditUserAction, - CALL_PHONE_NUMBER_ACTION, - VIEW_IN_MAPS_ACTION, - TRAVEL_GUIDE_ACTION, - PHONE_USER_ACTION, - EDIT_USER_ACTION, makePhoneCallAction, showcasePluggability, - SHOWCASE_PLUGGABILITY_ACTION, UserContext, CountryContext, PhoneContext, + EDIT_USER_ACTION, + SHOWCASE_PLUGGABILITY_ACTION, + CALL_PHONE_NUMBER_ACTION, + TRAVEL_GUIDE_ACTION, + VIEW_IN_MAPS_ACTION, + PHONE_USER_ACTION, } from './actions/actions'; interface StartDeps { @@ -54,6 +54,15 @@ declare module '../../../src/plugins/ui_actions/public' { [COUNTRY_TRIGGER]: CountryContext; [PHONE_TRIGGER]: PhoneContext; } + + export interface ActionContextMapping { + [EDIT_USER_ACTION]: UserContext; + [SHOWCASE_PLUGGABILITY_ACTION]: undefined; + [CALL_PHONE_NUMBER_ACTION]: PhoneContext; + [TRAVEL_GUIDE_ACTION]: CountryContext; + [VIEW_IN_MAPS_ACTION]: CountryContext; + [PHONE_USER_ACTION]: UserContext; + } } export class UiActionsExplorerPlugin implements Plugin { @@ -67,29 +76,26 @@ export class UiActionsExplorerPlugin implements Plugin (await startServices)[1].uiActions) ); - deps.uiActions.registerAction( + deps.uiActions.attachAction( + USER_TRIGGER, createEditUserAction(async () => (await startServices)[0].overlays.openModal) ); - deps.uiActions.attachAction(USER_TRIGGER, PHONE_USER_ACTION); - deps.uiActions.attachAction(USER_TRIGGER, EDIT_USER_ACTION); // What's missing here is type analysis to ensure the context emitted by the trigger // is the same context that the action requires. - deps.uiActions.attachAction(COUNTRY_TRIGGER, VIEW_IN_MAPS_ACTION); - deps.uiActions.attachAction(COUNTRY_TRIGGER, TRAVEL_GUIDE_ACTION); - deps.uiActions.attachAction(COUNTRY_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); - deps.uiActions.attachAction(PHONE_TRIGGER, CALL_PHONE_NUMBER_ACTION); - deps.uiActions.attachAction(PHONE_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); - deps.uiActions.attachAction(USER_TRIGGER, SHOWCASE_PLUGGABILITY_ACTION); + deps.uiActions.attachAction(COUNTRY_TRIGGER, viewInMapsAction); + deps.uiActions.attachAction(COUNTRY_TRIGGER, lookUpWeatherAction); + deps.uiActions.attachAction(COUNTRY_TRIGGER, showcasePluggability); + deps.uiActions.attachAction(PHONE_TRIGGER, makePhoneCallAction); + deps.uiActions.attachAction(PHONE_TRIGGER, showcasePluggability); + deps.uiActions.attachAction(USER_TRIGGER, showcasePluggability); core.application.register({ id: 'uiActionsExplorer', diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts index 7f1c5d78ab80..19712bf94c15 100644 --- a/src/legacy/core_plugins/data/public/actions/select_range_action.ts +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -19,21 +19,21 @@ import { i18n } from '@kbn/i18n'; import { - Action, createAction, IncompatibleActionError, + ActionByType, } from '../../../../../plugins/ui_actions/public'; import { onBrushEvent } from './filters/brush_event'; import { FilterManager, TimefilterContract, esFilters } from '../../../../../plugins/data/public'; export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; -interface ActionContext { +export interface SelectRangeActionContext { data: any; timeFieldName: string; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: SelectRangeActionContext) { try { return Boolean(await onBrushEvent(context.data)); } catch { @@ -44,8 +44,8 @@ async function isCompatible(context: ActionContext) { export function selectRangeAction( filterManager: FilterManager, timeFilter: TimefilterContract -): Action { - return createAction({ +): ActionByType { + return createAction({ type: SELECT_RANGE_ACTION, id: SELECT_RANGE_ACTION, getDisplayName: () => { @@ -54,7 +54,7 @@ export function selectRangeAction( }); }, isCompatible, - execute: async ({ timeFieldName, data }: ActionContext) => { + execute: async ({ timeFieldName, data }: SelectRangeActionContext) => { if (!(await isCompatible({ timeFieldName, data }))) { throw new IncompatibleActionError(); } diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts index 260b401e6d65..870002b32178 100644 --- a/src/legacy/core_plugins/data/public/actions/value_click_action.ts +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../plugins/kibana_react/public'; import { - Action, + ActionByType, createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; @@ -39,12 +39,12 @@ import { export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; -interface ActionContext { +export interface ValueClickActionContext { data: any; timeFieldName: string; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: ValueClickActionContext) { try { const filters: Filter[] = (await createFiltersFromEvent(context.data)) || []; return filters.length > 0; @@ -56,8 +56,8 @@ async function isCompatible(context: ActionContext) { export function valueClickAction( filterManager: FilterManager, timeFilter: TimefilterContract -): Action { - return createAction({ +): ActionByType { + return createAction({ type: VALUE_CLICK_ACTION, id: VALUE_CLICK_ACTION, getDisplayName: () => { @@ -66,7 +66,7 @@ export function valueClickAction( }); }, isCompatible, - execute: async ({ timeFieldName, data }: ActionContext) => { + execute: async ({ timeFieldName, data }: ValueClickActionContext) => { if (!(await isCompatible({ timeFieldName, data }))) { throw new IncompatibleActionError(); } diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index e2b8ca5dda78..7faa4a03acaa 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -37,8 +37,16 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; import { setSearchServiceShim } from './services'; -import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; -import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + selectRangeAction, + SelectRangeActionContext, + SELECT_RANGE_ACTION, +} from './actions/select_range_action'; +import { + valueClickAction, + VALUE_CLICK_ACTION, + ValueClickActionContext, +} from './actions/value_click_action'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, @@ -76,6 +84,12 @@ export interface DataSetup { export interface DataStart { search: SearchStart; } +declare module '../../../../plugins/ui_actions/public' { + export interface ActionContextMapping { + [SELECT_RANGE_ACTION]: SelectRangeActionContext; + [VALUE_CLICK_ACTION]: ValueClickActionContext; + } +} /** * Data Plugin - public @@ -100,10 +114,13 @@ export class DataPlugin // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); - uiActions.registerAction( + uiActions.attachAction( + SELECT_RANGE_TRIGGER, selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) ); - uiActions.registerAction( + + uiActions.attachAction( + VALUE_CLICK_TRIGGER, valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) ); @@ -123,9 +140,6 @@ export class DataPlugin setSearchService(data.search); setOverlays(core.overlays); - uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); - uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); - return { search, }; diff --git a/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx index edfba153b2b0..a5ca6808a3eb 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { IEmbeddable } from '../embeddable_plugin'; -import { Action, IncompatibleActionError } from '../ui_actions_plugin'; +import { ActionByType, IncompatibleActionError } from '../ui_actions_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; export const EXPAND_PANEL_ACTION = 'togglePanel'; @@ -36,18 +36,18 @@ function isExpanded(embeddable: IEmbeddable) { return embeddable.id === embeddable.parent.getInput().expandedPanelId; } -interface ActionContext { +export interface ExpandPanelActionContext { embeddable: IEmbeddable; } -export class ExpandPanelAction implements Action { +export class ExpandPanelAction implements ActionByType { public readonly type = EXPAND_PANEL_ACTION; public readonly id = EXPAND_PANEL_ACTION; public order = 7; constructor() {} - public getDisplayName({ embeddable }: ActionContext) { + public getDisplayName({ embeddable }: ExpandPanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } @@ -67,7 +67,7 @@ export class ExpandPanelAction implements Action { ); } - public getIconType({ embeddable }: ActionContext) { + public getIconType({ embeddable }: ExpandPanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } @@ -75,11 +75,11 @@ export class ExpandPanelAction implements Action { return isExpanded(embeddable) ? 'expand' : 'expand'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible({ embeddable }: ExpandPanelActionContext) { return Boolean(embeddable.parent && isDashboard(embeddable.parent)); } - public async execute({ embeddable }: ActionContext) { + public async execute({ embeddable }: ExpandPanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx index 16f611a2f1ff..81db4685288c 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; -import { Action, IncompatibleActionError } from '../ui_actions_plugin'; +import { ActionByType, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; export const REPLACE_PANEL_ACTION = 'replacePanel'; @@ -30,11 +30,11 @@ function isDashboard(embeddable: IEmbeddable): embeddable is DashboardContainer return embeddable.type === DASHBOARD_CONTAINER_TYPE; } -interface ActionContext { +export interface ReplacePanelActionContext { embeddable: IEmbeddable; } -export class ReplacePanelAction implements Action { +export class ReplacePanelAction implements ActionByType { public readonly type = REPLACE_PANEL_ACTION; public readonly id = REPLACE_PANEL_ACTION; public order = 11; @@ -46,7 +46,7 @@ export class ReplacePanelAction implements Action { private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] ) {} - public getDisplayName({ embeddable }: ActionContext) { + public getDisplayName({ embeddable }: ReplacePanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } @@ -55,14 +55,14 @@ export class ReplacePanelAction implements Action { }); } - public getIconType({ embeddable }: ActionContext) { + public getIconType({ embeddable }: ReplacePanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } return 'kqlOperand'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible({ embeddable }: ReplacePanelActionContext) { if (embeddable.getInput().viewMode) { if (embeddable.getInput().viewMode === ViewMode.VIEW) { return false; @@ -72,7 +72,7 @@ export class ReplacePanelAction implements Action { return Boolean(embeddable.parent && isDashboard(embeddable.parent)); } - public async execute({ embeddable }: ActionContext) { + public async execute({ embeddable }: ReplacePanelActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index 44c9dbf2dcc4..7629cc80c2f8 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -31,6 +31,8 @@ import { ExitFullScreenButton as ExitFullScreenButtonUi, ExitFullScreenButtonProps, } from '../../../plugins/kibana_react/public'; +import { ExpandPanelActionContext, EXPAND_PANEL_ACTION } from './actions/expand_panel_action'; +import { ReplacePanelActionContext, REPLACE_PANEL_ACTION } from './actions/replace_panel_action'; interface SetupDependencies { embeddable: IEmbeddableSetup; @@ -46,6 +48,13 @@ interface StartDependencies { export type Setup = void; export type Start = void; +declare module '../../../plugins/ui_actions/public' { + export interface ActionContextMapping { + [EXPAND_PANEL_ACTION]: ExpandPanelActionContext; + [REPLACE_PANEL_ACTION]: ReplacePanelActionContext; + } +} + export class DashboardEmbeddableContainerPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} @@ -53,7 +62,7 @@ export class DashboardEmbeddableContainerPublicPlugin public setup(core: CoreSetup, { embeddable, uiActions }: SetupDependencies): Setup { const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction); } public start(core: CoreStart, plugins: StartDependencies): Start { @@ -81,7 +90,7 @@ export class DashboardEmbeddableContainerPublicPlugin plugins.embeddable.getEmbeddableFactories ); uiActions.registerAction(changeViewAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction.id); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction); const factory = new DashboardContainerFactory({ application, diff --git a/src/plugins/dashboard_embeddable_container/public/tests/dashboard_container.test.tsx b/src/plugins/dashboard_embeddable_container/public/tests/dashboard_container.test.tsx index 6a3b69af60d6..a81d80b440e0 100644 --- a/src/plugins/dashboard_embeddable_container/public/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard_embeddable_container/public/tests/dashboard_container.test.tsx @@ -49,7 +49,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { const editModeAction = createEditModeAction(); uiActionsSetup.registerAction(editModeAction); - uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction.id); + uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction); setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any) diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index 6edb3237987f..c698d56c0fdd 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -19,27 +19,27 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../kibana_react/public'; -import { Action, createAction, IncompatibleActionError } from '../../../ui_actions/public'; +import { ActionByType, 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 GLOBAL_APPLY_FILTER_ACTION = 'GLOBAL_APPLY_FILTER_ACTION'; -interface ActionContext { +export interface ApplyGlobalFilterActionContext { filters: Filter[]; timeFieldName?: string; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: ApplyGlobalFilterActionContext) { return context.filters !== undefined; } export function createFilterAction( filterManager: FilterManager, timeFilter: TimefilterContract -): Action { - return createAction({ +): ActionByType { + return createAction({ type: GLOBAL_APPLY_FILTER_ACTION, id: GLOBAL_APPLY_FILTER_ACTION, getDisplayName: () => { @@ -48,7 +48,7 @@ export function createFilterAction( }); }, isCompatible, - execute: async ({ filters, timeFieldName }: ActionContext) => { + execute: async ({ filters, timeFieldName }: ApplyGlobalFilterActionContext) => { 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 8ce379547ead..367d70d220ea 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -47,6 +47,13 @@ import { import { createFilterAction, GLOBAL_APPLY_FILTER_ACTION } from './actions'; import { APPLY_FILTER_TRIGGER } from '../../embeddable/public'; import { createSearchBar } from './ui/search_bar/create_search_bar'; +import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action'; + +declare module '../../ui_actions/public' { + export interface ActionContextMapping { + [GLOBAL_APPLY_FILTER_ACTION]: ApplyGlobalFilterActionContext; + } +} export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteService(); @@ -93,7 +100,7 @@ export class DataPublicPlugin implements Plugin; -interface ActionContext { +export interface FilterActionContext { embeddable: IEmbeddable; filters: Filter[]; } -async function isCompatible(context: ActionContext) { +async function isCompatible(context: FilterActionContext) { if (context.embeddable === undefined) { return false; } @@ -38,8 +38,8 @@ async function isCompatible(context: ActionContext) { return Boolean(root.getInput().filters !== undefined && context.filters !== undefined); } -export function createFilterAction(): Action { - return createAction({ +export function createFilterAction(): ActionByType { + return createAction({ type: APPLY_FILTER_ACTION, id: APPLY_FILTER_ACTION, getDisplayName: () => { diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 218660462b4e..fdff82e63fae 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -25,7 +25,7 @@ import { nextTick } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; -import { Action, UiActionsStart } from 'src/plugins/ui_actions/public'; +import { Action, UiActionsStart, ActionType } from 'src/plugins/ui_actions/public'; import { Trigger, GetEmbeddableFactory, ViewMode } from '../types'; import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables'; import { EmbeddablePanel } from './embeddable_panel'; @@ -213,9 +213,9 @@ const renderInEditModeAndOpenContextMenu = async ( }; test('HelloWorldContainer in edit mode hides disabledActions', async () => { - const action = { + const action: Action = { id: 'FOO', - type: 'FOO', + type: 'FOO' as ActionType, getIconType: () => undefined, getDisplayName: () => 'foo', isCompatible: async () => true, @@ -245,9 +245,9 @@ test('HelloWorldContainer in edit mode hides disabledActions', async () => { }); test('HelloWorldContainer hides disabled badges', async () => { - const action = { + const action: Action = { id: 'BAR', - type: 'BAR', + type: 'BAR' as ActionType, getIconType: () => undefined, getDisplayName: () => 'bar', isCompatible: async () => true, diff --git a/src/plugins/embeddable/public/lib/panel/index.ts b/src/plugins/embeddable/public/lib/panel/index.ts index dee52bc5bec5..f5ef8d9e20ed 100644 --- a/src/plugins/embeddable/public/lib/panel/index.ts +++ b/src/plugins/embeddable/public/lib/panel/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { EmbeddablePanel } from './embeddable_panel'; -export { ADD_PANEL_ACTION_ID, AddPanelAction, openAddPanelFlyout } from './panel_header'; +export * from './embeddable_panel'; +export * from './panel_header'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/index.ts b/src/plugins/embeddable/public/lib/panel/panel_header/index.ts index e5975b06ba1e..d64094f2d5e2 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/index.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/index.ts @@ -17,9 +17,4 @@ * under the License. */ -export { - ADD_PANEL_ACTION_ID, - AddPanelAction, - RemovePanelAction, - openAddPanelFlyout, -} from './panel_actions'; +export * from './panel_actions'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts index e0d34fc1f4b0..719c0dff2a2b 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts @@ -22,7 +22,7 @@ import { Action } from 'src/plugins/ui_actions/public'; import { ViewMode } from '../../../../types'; import { IEmbeddable } from '../../../../embeddables'; -const CUSTOMIZE_PANEL_ACTION_ID = 'CUSTOMIZE_PANEL_ACTION_ID'; +export const CUSTOMIZE_PANEL_ACTION_ID = 'CUSTOMIZE_PANEL_ACTION_ID'; type GetUserData = (context: ActionContext) => Promise<{ title: string | undefined }>; diff --git a/src/plugins/ui_actions/public/tests/test_samples/restricted_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/index.ts similarity index 69% rename from src/plugins/ui_actions/public/tests/test_samples/restricted_action.ts rename to src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/index.ts index aa65d3af9816..2aa4253e988d 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/restricted_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/index.ts @@ -17,14 +17,4 @@ * under the License. */ -import { Action, createAction } from '../../actions'; - -export const RESTRICTED_ACTION = 'RESTRICTED_ACTION'; - -export function createRestrictedAction(isCompatibleIn: (context: C) => boolean): Action { - return createAction({ - type: RESTRICTED_ACTION, - isCompatible: async context => isCompatibleIn(context), - execute: async () => {}, - }); -} +export * from './customize_panel_action'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts index 7810e0095b63..27e9dd903848 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts @@ -17,6 +17,7 @@ * under the License. */ -export { InspectPanelAction } from './inspect_panel_action'; -export { ADD_PANEL_ACTION_ID, AddPanelAction, openAddPanelFlyout } from './add_panel'; -export { RemovePanelAction } from './remove_panel_action'; +export * from './inspect_panel_action'; +export * from './add_panel'; +export * from './remove_panel_action'; +export * from './customize_title'; diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts b/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts index b5ceae0c15a2..434942529ac5 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts +++ b/src/plugins/embeddable/public/lib/test_samples/actions/edit_mode_action.ts @@ -17,17 +17,18 @@ * under the License. */ -import { createAction } from '../../ui_actions'; +import { createAction, ActionType } from '../../ui_actions'; import { ViewMode } from '../../types'; -import { EmbeddableContext } from '../../triggers'; +import { IEmbeddable } from '../..'; -export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION'; +export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION' as ActionType; export function createEditModeAction() { - return createAction({ + return createAction({ type: EDIT_MODE_ACTION, getDisplayName: () => 'I only show up in edit mode', - isCompatible: async context => context.embeddable.getInput().viewMode === ViewMode.EDIT, + isCompatible: async (context: { embeddable: IEmbeddable }) => + context.embeddable.getInput().viewMode === ViewMode.EDIT, execute: async () => {}, }); } diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx index 55615875528a..d1044484c98f 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx @@ -17,10 +17,10 @@ * under the License. */ -import { Action, IncompatibleActionError } from '../../ui_actions'; +import { ActionByType, IncompatibleActionError, ActionType } from '../../ui_actions'; import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables'; -export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION'; +export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION' as ActionType; export interface FullNameEmbeddableOutput extends EmbeddableOutput { fullName: string; @@ -35,12 +35,12 @@ export function hasFullNameOutput( ); } -interface ActionContext { +export interface SayHelloActionContext { embeddable: Embeddable; message?: string; } -export class SayHelloAction implements Action { +export class SayHelloAction implements ActionByType { public readonly type = SAY_HELLO_ACTION; public readonly id = SAY_HELLO_ACTION; @@ -62,7 +62,7 @@ export class SayHelloAction implements Action { // Can use typescript generics to get compiler time warnings for immediate feedback if // the context is not compatible. - async isCompatible(context: ActionContext) { + async isCompatible(context: SayHelloActionContext) { // Option 1: only compatible with Greeting Embeddables. // return context.embeddable.type === CONTACT_CARD_EMBEDDABLE; @@ -70,7 +70,7 @@ export class SayHelloAction implements Action { return hasFullNameOutput(context.embeddable); } - async execute(context: ActionContext) { + async execute(context: SayHelloActionContext) { if (!(await this.isCompatible(context))) { throw new IncompatibleActionError(); } diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx b/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx index 502269d7ac19..6fdf6244a951 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/actions/send_message_action.tsx @@ -18,14 +18,14 @@ */ import React from 'react'; import { EuiFlyoutBody } from '@elastic/eui'; -import { createAction, IncompatibleActionError } from '../../ui_actions'; +import { createAction, IncompatibleActionError, ActionType } from '../../ui_actions'; import { CoreStart } from '../../../../../../core/public'; import { toMountPoint } from '../../../../../kibana_react/public'; import { Embeddable, EmbeddableInput } from '../../embeddables'; import { GetMessageModal } from './get_message_modal'; import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action'; -export const SEND_MESSAGE_ACTION = 'SEND_MESSAGE_ACTION'; +export const SEND_MESSAGE_ACTION = 'SEND_MESSAGE_ACTION' as ActionType; interface ActionContext { embeddable: Embeddable; @@ -42,11 +42,11 @@ export function createSendMessageAction(overlays: CoreStart['overlays']) { overlays.openFlyout(toMountPoint({content})); }; - return createAction({ + return createAction({ type: SEND_MESSAGE_ACTION, getDisplayName: () => 'Send message', isCompatible, - execute: async context => { + execute: async (context: ActionContext) => { if (!(await isCompatible(context))) { throw new IncompatibleActionError(); } diff --git a/src/plugins/ui_actions/public/actions/action.test.ts b/src/plugins/ui_actions/public/actions/action.test.ts index e1a789ae1cc4..fcf023507f91 100644 --- a/src/plugins/ui_actions/public/actions/action.test.ts +++ b/src/plugins/ui_actions/public/actions/action.test.ts @@ -17,17 +17,21 @@ * under the License. */ -import { createSayHelloAction } from '../tests/test_samples/say_hello_action'; +import { createAction } from '../../../ui_actions/public'; +import { ActionType } from '../types'; -test('SayHelloAction is not compatible with not matching context', async () => { - const sayHelloAction = createSayHelloAction((() => {}) as any); +const sayHelloAction = createAction({ + type: 'test' as ActionType, + isCompatible: ({ amICompatible }: { amICompatible: boolean }) => Promise.resolve(amICompatible), + execute: () => Promise.resolve(), +}); - const isCompatible = await sayHelloAction.isCompatible({} as any); +test('action is not compatible based on context', async () => { + const isCompatible = await sayHelloAction.isCompatible({ amICompatible: false }); expect(isCompatible).toBe(false); }); -test('HelloWorldAction inherits isCompatible from base action', async () => { - const helloWorldAction = createSayHelloAction({} as any); - const isCompatible = await helloWorldAction.isCompatible({ name: 'Sue' }); +test('action is compatible based on context', async () => { + const isCompatible = await sayHelloAction.isCompatible({ amICompatible: true }); expect(isCompatible).toBe(true); }); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index 854e2c8c1cb0..4ad692b21627 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -18,17 +18,26 @@ */ import { UiComponent } from 'src/plugins/kibana_utils/common'; +import { ActionType, ActionContextMapping } from '../types'; -export interface Action { +export type ActionByType = Action; + +export interface Action { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. */ order?: number; + /** + * A unique identifier for this action instance. + */ id: string; - readonly type: string; + /** + * The action type is what determines the context shape. + */ + readonly type: T; /** * Optional EUI icon type that can be displayed along with the title. diff --git a/src/plugins/ui_actions/public/actions/action_definition.ts b/src/plugins/ui_actions/public/actions/action_definition.ts new file mode 100644 index 000000000000..c590cf8f34ee --- /dev/null +++ b/src/plugins/ui_actions/public/actions/action_definition.ts @@ -0,0 +1,72 @@ +/* + * 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 { UiComponent } from 'src/plugins/kibana_utils/common'; +import { ActionType, ActionContextMapping } from '../types'; + +export interface ActionDefinition { + /** + * Determined the order when there is more than one action matched to a trigger. + * Higher numbers are displayed first. + */ + order?: number; + + /** + * A unique identifier for this action instance. + */ + id?: string; + + /** + * The action type is what determines the context shape. + */ + readonly type: T; + + /** + * Optional EUI icon type that can be displayed along with the title. + */ + getIconType?(context: ActionContextMapping[T]): string; + + /** + * Returns a title to be displayed to the user. + * @param context + */ + getDisplayName?(context: ActionContextMapping[T]): string; + + /** + * `UiComponent` to render when displaying this action as a context menu item. + * If not provided, `getDisplayName` will be used instead. + */ + MenuItem?: UiComponent<{ context: ActionContextMapping[T] }>; + + /** + * Returns a promise that resolves to true if this action is compatible given the context, + * otherwise resolves to false. + */ + isCompatible?(context: ActionContextMapping[T]): Promise; + + /** + * If this returns something truthy, this is used in addition to the `execute` method when clicked. + */ + getHref?(context: ActionContextMapping[T]): string | undefined; + + /** + * Executes the action. + */ + execute(context: ActionContextMapping[T]): Promise; +} diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index 4077cf108102..90a9415c0b49 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -17,11 +17,11 @@ * under the License. */ -import { Action } from './action'; +import { ActionByType } from './action'; +import { ActionType } from '../types'; +import { ActionDefinition } from './action_definition'; -export function createAction( - action: { type: string; execute: Action['execute'] } & Partial> -): Action { +export function createAction(action: ActionDefinition): ActionByType { return { getIconType: () => undefined, order: 0, diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index eb69aefdbb50..79b8e1474f6c 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -29,4 +29,5 @@ export { UiActionsServiceParams, UiActionsService } from './service'; export { Action, createAction, IncompatibleActionError } from './actions'; export { buildContextMenuForActions } from './context_menu'; export { Trigger, TriggerContext } from './triggers'; -export { TriggerContextMapping, TriggerId } from './types'; +export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types'; +export { ActionByType } from './actions'; diff --git a/src/plugins/ui_actions/public/mocks.ts b/src/plugins/ui_actions/public/mocks.ts index 948450495384..c1be6b262652 100644 --- a/src/plugins/ui_actions/public/mocks.ts +++ b/src/plugins/ui_actions/public/mocks.ts @@ -41,6 +41,7 @@ const createStartContract = (): Start => { attachAction: jest.fn(), registerAction: jest.fn(), registerTrigger: jest.fn(), + getAction: jest.fn(), detachAction: jest.fn(), executeTriggerActions: jest.fn(), getTrigger: jest.fn(), diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts index c52b97535861..4521ea53b482 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts @@ -18,9 +18,9 @@ */ import { UiActionsService } from './ui_actions_service'; -import { Action } from '../actions'; -import { createRestrictedAction, createHelloWorldAction } from '../tests/test_samples'; -import { ActionRegistry, TriggerRegistry, TriggerId } from '../types'; +import { Action, createAction } from '../actions'; +import { createHelloWorldAction } from '../tests/test_samples'; +import { ActionRegistry, TriggerRegistry, TriggerId, ActionType } from '../types'; import { Trigger } from '../triggers'; // I tried redeclaring the module in here to extend the `TriggerContextMapping` but @@ -33,7 +33,7 @@ const MY_TRIGGER: TriggerId = 'MY_TRIGGER' as TriggerId; const testAction1: Action = { id: 'action1', order: 1, - type: 'type1', + type: 'type1' as ActionType, execute: async () => {}, getDisplayName: () => 'test1', getIconType: () => '', @@ -43,7 +43,7 @@ const testAction1: Action = { const testAction2: Action = { id: 'action2', order: 2, - type: 'type2', + type: 'type2' as ActionType, execute: async () => {}, getDisplayName: () => 'test2', getIconType: () => '', @@ -100,25 +100,25 @@ describe('UiActionsService', () => { getDisplayName: () => 'test', getIconType: () => '', isCompatible: async () => true, - type: 'test', + type: 'test' as ActionType, }); }); }); describe('.getTriggerActions()', () => { - const action1: Action = { + const action1: Action = { id: 'action1', order: 1, - type: 'type1', + type: 'type1' as ActionType, execute: async () => {}, getDisplayName: () => 'test', getIconType: () => '', isCompatible: async () => true, }; - const action2: Action = { + const action2: Action = { id: 'action2', order: 2, - type: 'type2', + type: 'type2' as ActionType, execute: async () => {}, getDisplayName: () => 'test', getIconType: () => '', @@ -140,13 +140,13 @@ describe('UiActionsService', () => { expect(list0).toHaveLength(0); - service.attachAction(FOO_TRIGGER, 'action1'); + service.attachAction(FOO_TRIGGER, action1); const list1 = service.getTriggerActions(FOO_TRIGGER); expect(list1).toHaveLength(1); expect(list1).toEqual([action1]); - service.attachAction(FOO_TRIGGER, 'action2'); + service.attachAction(FOO_TRIGGER, action2); const list2 = service.getTriggerActions(FOO_TRIGGER); expect(list2).toHaveLength(2); @@ -179,7 +179,7 @@ describe('UiActionsService', () => { title: 'My trigger', }; service.registerTrigger(testTrigger); - service.attachAction(MY_TRIGGER, helloWorldAction.id); + service.attachAction(MY_TRIGGER, helloWorldAction); const compatibleActions = await service.getTriggerCompatibleActions(MY_TRIGGER, { hi: 'there', @@ -191,11 +191,13 @@ describe('UiActionsService', () => { test('filters out actions not applicable based on the context', async () => { const service = new UiActionsService(); - const restrictedAction = createRestrictedAction<{ accept: boolean }>(context => { - return context.accept; + const action = createAction({ + type: 'test' as ActionType, + isCompatible: ({ accept }: { accept: boolean }) => Promise.resolve(accept), + execute: () => Promise.resolve(), }); - service.registerAction(restrictedAction); + service.registerAction(action); const testTrigger: Trigger = { id: MY_TRIGGER, @@ -203,7 +205,7 @@ describe('UiActionsService', () => { }; service.registerTrigger(testTrigger); - service.attachAction(testTrigger.id, restrictedAction.id); + service.attachAction(testTrigger.id, action); const compatibleActions1 = await service.getTriggerCompatibleActions(testTrigger.id, { accept: true, @@ -287,7 +289,7 @@ describe('UiActionsService', () => { id: FOO_TRIGGER, }); service1.registerAction(testAction1); - service1.attachAction(FOO_TRIGGER, testAction1.id); + service1.attachAction(FOO_TRIGGER, testAction1); const service2 = service1.fork(); @@ -308,14 +310,14 @@ describe('UiActionsService', () => { }); service1.registerAction(testAction1); service1.registerAction(testAction2); - service1.attachAction(FOO_TRIGGER, testAction1.id); + service1.attachAction(FOO_TRIGGER, testAction1); const service2 = service1.fork(); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); - service2.attachAction(FOO_TRIGGER, testAction2.id); + service2.attachAction(FOO_TRIGGER, testAction2); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(2); @@ -329,14 +331,14 @@ describe('UiActionsService', () => { }); service1.registerAction(testAction1); service1.registerAction(testAction2); - service1.attachAction(FOO_TRIGGER, testAction1.id); + service1.attachAction(FOO_TRIGGER, testAction1); const service2 = service1.fork(); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); - service1.attachAction(FOO_TRIGGER, testAction2.id); + service1.attachAction(FOO_TRIGGER, testAction2); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(2); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); @@ -391,8 +393,7 @@ describe('UiActionsService', () => { } as any; service.registerTrigger(trigger); - service.registerAction(action); - service.attachAction(MY_TRIGGER, HELLO_WORLD_ACTION_ID); + service.attachAction(MY_TRIGGER, action); const actions = service.getTriggerActions(trigger.id); @@ -413,8 +414,8 @@ describe('UiActionsService', () => { service.registerTrigger(trigger); service.registerAction(action); - service.attachAction(trigger.id, HELLO_WORLD_ACTION_ID); - service.detachAction(trigger.id, HELLO_WORLD_ACTION_ID); + service.attachAction(trigger.id, action); + service.detachAction(trigger.id, action.id); const actions2 = service.getTriggerActions(trigger.id); expect(actions2).toEqual([]); @@ -445,9 +446,7 @@ describe('UiActionsService', () => { } as any; service.registerAction(action); - expect(() => - service.attachAction('i do not exist' as TriggerId, HELLO_WORLD_ACTION_ID) - ).toThrowError( + expect(() => service.attachAction('i do not exist' as TriggerId, action)).toThrowError( 'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = HELLO_WORLD_ACTION_ID].' ); }); 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 66f038f05a4a..1c65303a2df9 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -23,8 +23,9 @@ import { TriggerToActionsRegistry, TriggerId, TriggerContextMapping, + ActionType, } from '../types'; -import { Action } from '../actions'; +import { Action, ActionByType } from '../actions'; import { Trigger, TriggerContext } from '../triggers/trigger'; import { TriggerInternal } from '../triggers/trigger_internal'; import { TriggerContract } from '../triggers/trigger_contract'; @@ -75,7 +76,7 @@ export class UiActionsService { return trigger.contract; }; - public readonly registerAction = (action: Action) => { + public readonly registerAction = (action: ActionByType) => { if (this.actions.has(action.id)) { throw new Error(`Action [action.id = ${action.id}] already registered.`); } @@ -83,22 +84,41 @@ export class UiActionsService { this.actions.set(action.id, action); }; - // TODO: make this - // (triggerId: T, action: Action): \ - // to get type checks here! - public readonly attachAction = (triggerId: T, actionId: string): void => { + public readonly getAction = (id: string): ActionByType => { + if (!this.actions.has(id)) { + throw new Error(`Action [action.id = ${id}] not registered.`); + } + + return this.actions.get(id) as ActionByType; + }; + + public readonly attachAction = ( + triggerId: TType, + // 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 | undefined> & ActionByType + ): void => { + if (!this.actions.has(action.id)) { + this.registerAction(action); + } else { + const registeredAction = this.actions.get(action.id); + if (registeredAction !== action) { + throw new Error(`A different action instance with this id is already registered.`); + } + } + const trigger = this.triggers.get(triggerId); if (!trigger) { throw new Error( - `No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${actionId}].` + `No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${action.id}].` ); } const actionIds = this.triggerToActions.get(triggerId); - if (!actionIds!.find(id => id === actionId)) { - this.triggerToActions.set(triggerId, [...actionIds!, actionId]); + if (!actionIds!.find(id => id === action.id)) { + this.triggerToActions.set(triggerId, [...actionIds!, action.id]); } }; @@ -137,7 +157,7 @@ export class UiActionsService { public readonly getTriggerCompatibleActions = async ( triggerId: T, context: TriggerContextMapping[T] - ): Promise>> => { + ): Promise>> => { const actions = this.getTriggerActions!(triggerId); const isCompatibles = await Promise.all(actions.map(action => action.isCompatible(context))); return actions.reduce( diff --git a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts index 450bfbfc6c95..94cafe14524f 100644 --- a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts @@ -21,7 +21,7 @@ import { Action, createAction } from '../actions'; import { openContextMenu } from '../context_menu'; import { uiActionsPluginMock } from '../mocks'; import { Trigger } from '../triggers'; -import { TriggerId } from '../types'; +import { TriggerId, ActionType } from '../types'; jest.mock('../context_menu'); @@ -30,11 +30,16 @@ const openContextMenuSpy = (openContextMenu as any) as jest.SpyInstance; const CONTACT_USER_TRIGGER = 'CONTACT_USER_TRIGGER'; -function createTestAction(id: string, checkCompatibility: (context: A) => boolean): Action { - return createAction({ - type: 'testAction', - id, - isCompatible: context => Promise.resolve(checkCompatibility(context)), +const TEST_ACTION_TYPE = 'TEST_ACTION_TYPE' as ActionType; + +function createTestAction( + type: string, + checkCompatibility: (context: C) => boolean +): Action { + return createAction({ + type: type as ActionType, + id: type, + isCompatible: (context: C) => Promise.resolve(checkCompatibility(context)), execute: context => executeFn(context), }); } @@ -46,7 +51,7 @@ const reset = () => { uiActions.setup.registerTrigger({ id: CONTACT_USER_TRIGGER, }); - uiActions.setup.attachAction(CONTACT_USER_TRIGGER, 'SEND_MESSAGE_ACTION'); + // uiActions.setup.attachAction(CONTACT_USER_TRIGGER, 'SEND_MESSAGE_ACTION'); executeFn.mockReset(); openContextMenuSpy.mockReset(); @@ -62,8 +67,7 @@ test('executes a single action mapped to a trigger', async () => { const action = createTestAction('test1', () => true); setup.registerTrigger(trigger); - setup.registerAction(action); - setup.attachAction(trigger.id, 'test1'); + setup.attachAction(trigger.id, action); const context = {}; const start = doStart(); @@ -81,7 +85,6 @@ test('throws an error if there are no compatible actions to execute', async () = }; setup.registerTrigger(trigger); - setup.attachAction(trigger.id, 'testaction'); const context = {}; const start = doStart(); @@ -98,11 +101,13 @@ test('does not execute an incompatible action', async () => { id: 'MY-TRIGGER' as TriggerId, title: 'My trigger', }; - const action = createTestAction<{ name: string }>('test1', ({ name }) => name === 'executeme'); + const action = createTestAction<{ name: string }>( + 'test1', + ({ name }: { name: string }) => name === 'executeme' + ); setup.registerTrigger(trigger); - setup.registerAction(action); - setup.attachAction(trigger.id, 'test1'); + setup.attachAction(trigger.id, action); const start = doStart(); const context = { @@ -123,10 +128,8 @@ test('shows a context menu when more than one action is mapped to a trigger', as const action2 = createTestAction('test2', () => true); setup.registerTrigger(trigger); - setup.registerAction(action1); - setup.registerAction(action2); - setup.attachAction(trigger.id, 'test1'); - setup.attachAction(trigger.id, 'test2'); + setup.attachAction(trigger.id, action1); + setup.attachAction(trigger.id, action2); expect(openContextMenu).toHaveBeenCalledTimes(0); @@ -150,8 +153,7 @@ test('passes whole action context to isCompatible()', async () => { }); setup.registerTrigger(trigger); - setup.registerAction(action); - setup.attachAction(trigger.id, 'test'); + setup.attachAction(trigger.id, action); const start = doStart(); diff --git a/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts index ae335de4b3de..f5a6a96fb41a 100644 --- a/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts @@ -19,17 +19,17 @@ import { Action } from '../actions'; import { uiActionsPluginMock } from '../mocks'; -import { TriggerId } from '../types'; +import { TriggerId, ActionType } from '../types'; const action1: Action = { id: 'action1', order: 1, - type: 'type1', + type: 'type1' as ActionType, } as any; const action2: Action = { id: 'action2', order: 2, - type: 'type2', + type: 'type2' as ActionType, } as any; test('returns actions set on trigger', () => { @@ -47,13 +47,13 @@ test('returns actions set on trigger', () => { expect(list0).toHaveLength(0); - setup.attachAction('trigger' as TriggerId, 'action1'); + setup.attachAction('trigger' as TriggerId, action1); const list1 = start.getTriggerActions('trigger' as TriggerId); expect(list1).toHaveLength(1); expect(list1).toEqual([action1]); - setup.attachAction('trigger' as TriggerId, 'action2'); + setup.attachAction('trigger' as TriggerId, action2); const list2 = start.getTriggerActions('trigger' as TriggerId); expect(list2).toHaveLength(2); diff --git a/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts b/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts index dfb55e42b944..c5e68e5d5ca5 100644 --- a/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts @@ -17,25 +17,27 @@ * under the License. */ -import { createSayHelloAction } from '../tests/test_samples/say_hello_action'; import { uiActionsPluginMock } from '../mocks'; -import { createRestrictedAction, createHelloWorldAction } from '../tests/test_samples'; -import { Action } from '../actions'; +import { createHelloWorldAction } from '../tests/test_samples'; +import { Action, createAction } from '../actions'; import { Trigger } from '../triggers'; -import { TriggerId } from '../types'; +import { TriggerId, ActionType } from '../types'; -let action: Action<{ name: string }>; +let action: Action<{ name: string }, ActionType>; let uiActions: ReturnType; beforeEach(() => { uiActions = uiActionsPluginMock.createPlugin(); - action = createSayHelloAction({} as any); + action = createAction({ + type: 'test' as ActionType, + execute: () => Promise.resolve(), + }); uiActions.setup.registerAction(action); uiActions.setup.registerTrigger({ id: 'trigger' as TriggerId, title: 'trigger', }); - uiActions.setup.attachAction('trigger' as TriggerId, action.id); + uiActions.setup.attachAction('trigger' as TriggerId, action); }); test('can register action', async () => { @@ -56,7 +58,7 @@ test('getTriggerCompatibleActions returns attached actions', async () => { title: 'My trigger', }; setup.registerTrigger(testTrigger); - setup.attachAction('MY-TRIGGER' as TriggerId, helloWorldAction.id); + setup.attachAction('MY-TRIGGER' as TriggerId, helloWorldAction); const start = doStart(); const actions = await start.getTriggerCompatibleActions('MY-TRIGGER' as TriggerId, {}); @@ -67,19 +69,22 @@ test('getTriggerCompatibleActions returns attached actions', async () => { test('filters out actions not applicable based on the context', async () => { const { setup, doStart } = uiActions; - const restrictedAction = createRestrictedAction<{ accept: boolean }>(context => { - return context.accept; + const action1 = createAction({ + type: 'test1' as ActionType, + isCompatible: async (context: { accept: boolean }) => { + return Promise.resolve(context.accept); + }, + execute: () => Promise.resolve(), }); - setup.registerAction(restrictedAction); - const testTrigger: Trigger = { - id: 'MY-TRIGGER' as TriggerId, + id: 'MY-TRIGGER2' as TriggerId, title: 'My trigger', }; setup.registerTrigger(testTrigger); - setup.attachAction(testTrigger.id, restrictedAction.id); + setup.registerAction(action1); + setup.attachAction(testTrigger.id, action1); const start = doStart(); let actions = await start.getTriggerCompatibleActions(testTrigger.id, { accept: true }); diff --git a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx index 196f3e2d5cdc..d78e35ae76f4 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx +++ b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx @@ -20,8 +20,9 @@ import React from 'react'; import { EuiFlyout, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; import { CoreStart } from 'src/core/public'; -import { createAction, Action } from '../../actions'; +import { createAction, ActionByType } from '../../actions'; import { toMountPoint, reactToUiComponent } from '../../../../kibana_react/public'; +import { ActionType } from '../../types'; const ReactMenuItem: React.FC = () => { return ( @@ -36,10 +37,12 @@ const ReactMenuItem: React.FC = () => { const UiMenuItem = reactToUiComponent(ReactMenuItem); -export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID'; +export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID' as ActionType; -export function createHelloWorldAction(overlays: CoreStart['overlays']): Action { - return createAction({ +export function createHelloWorldAction( + overlays: CoreStart['overlays'] +): ActionByType { + return createAction({ type: HELLO_WORLD_ACTION_ID, getIconType: () => 'lock', MenuItem: UiMenuItem, diff --git a/src/plugins/ui_actions/public/tests/test_samples/index.ts b/src/plugins/ui_actions/public/tests/test_samples/index.ts index 40301d629aa4..7d63b1b6d566 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/index.ts +++ b/src/plugins/ui_actions/public/tests/test_samples/index.ts @@ -16,6 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -export { createRestrictedAction } from './restricted_action'; -export { createSayHelloAction } from './say_hello_action'; export { createHelloWorldAction } from './hello_world_action'; diff --git a/src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx b/src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx deleted file mode 100644 index f1265fed54b3..000000000000 --- a/src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 React from 'react'; -import { EuiFlyout } from '@elastic/eui'; -import { CoreStart } from 'src/core/public'; -import { Action, createAction } from '../../actions'; -import { toMountPoint } from '../../../../kibana_react/public'; - -export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION'; - -export function createSayHelloAction(overlays: CoreStart['overlays']): Action<{ name: string }> { - return createAction<{ name: string }>({ - type: SAY_HELLO_ACTION, - getDisplayName: ({ name }) => `Hello, ${name}`, - isCompatible: async ({ name }) => name !== undefined, - execute: async context => { - const flyoutSession = overlays.openFlyout( - toMountPoint( - flyoutSession && flyoutSession.close()}> - this.getDisplayName(context) - - ), - { - 'data-test-subj': 'sayHelloAction', - } - ); - }, - }); -} diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index d78d3c895122..23945b253e8d 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -17,11 +17,11 @@ * under the License. */ -import { Action } from './actions/action'; +import { ActionByType } from './actions/action'; import { TriggerInternal } from './triggers/trigger_internal'; export type TriggerRegistry = Map>; -export type ActionRegistry = Map>; +export type ActionRegistry = Map>; export type TriggerToActionsRegistry = Map; const DEFAULT_TRIGGER = ''; @@ -34,3 +34,10 @@ export type BaseContext = object | undefined | string | number; export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; } + +const DEFAULT_ACTION = ''; +export type ActionType = keyof ActionContextMapping; + +export interface ActionContextMapping { + [DEFAULT_ACTION]: BaseContext; +} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index 2c58abba6055..25666dc0359d 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -85,7 +85,7 @@ export class EmbeddableExplorerPublicPlugin plugins.uiActions.registerAction(sayHelloAction); plugins.uiActions.registerAction(sendMessageAction); - plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction.id); + plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction); plugins.embeddable.registerEmbeddableFactory( helloWorldEmbeddableFactory.type, diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx index 4ce748e2c711..8150fa34d654 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx @@ -21,18 +21,20 @@ import React from 'react'; import { npStart, npSetup } from 'ui/new_platform'; import { CONTEXT_MENU_TRIGGER, IEmbeddable } from '../../../../../src/plugins/embeddable/public'; -import { createAction } from '../../../../../src/plugins/ui_actions/public'; +import { createAction, ActionType } from '../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -interface ActionContext { +export const SAMPLE_PANEL_ACTION = 'SAMPLE_PANEL_ACTION' as ActionType; + +export interface SamplePanelActionContext { embeddable: IEmbeddable; } function createSamplePanelAction() { - return createAction({ - type: 'samplePanelAction', + return createAction({ + type: SAMPLE_PANEL_ACTION, getDisplayName: () => 'Sample Panel Action', - execute: async ({ embeddable }) => { + execute: async ({ embeddable }: SamplePanelActionContext) => { if (!embeddable) { return; } @@ -59,4 +61,4 @@ function createSamplePanelAction() { const action = createSamplePanelAction(); npSetup.plugins.uiActions.registerAction(action); -npSetup.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action.id); +npSetup.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action); diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts index 7a3fb7fa8554..618326ff9d07 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts @@ -17,12 +17,14 @@ * under the License. */ import { npStart } from 'ui/new_platform'; -import { Action, createAction } from '../../../../../src/plugins/ui_actions/public'; +import { Action, createAction, ActionType } from '../../../../../src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../../src/plugins/embeddable/public'; +export const SAMPLE_PANEL_LINK = 'samplePanelLink' as ActionType; + export const createSamplePanelLink = (): Action => - createAction({ - type: 'samplePanelLink', + createAction({ + type: SAMPLE_PANEL_LINK, getDisplayName: () => 'Sample panel Link', execute: async () => {}, getHref: () => 'https://example.com/kibana/test', @@ -30,4 +32,4 @@ export const createSamplePanelLink = (): Action => const action = createSamplePanelLink(); npStart.plugins.uiActions.registerAction(action); -npStart.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action.id); +npStart.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action); diff --git a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index f8d8fdf481dd..4c9cd890ee75 100644 --- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -8,7 +8,10 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import { npSetup, npStart } from 'ui/new_platform'; -import { Action, IncompatibleActionError } from '../../../../../../src/plugins/ui_actions/public'; +import { + ActionByType, + IncompatibleActionError, +} from '../../../../../../src/plugins/ui_actions/public'; import { ViewMode, @@ -28,11 +31,17 @@ function isSavedSearchEmbeddable( return embeddable.type === SEARCH_EMBEDDABLE_TYPE; } -interface ActionContext { +export interface CSVActionContext { embeddable: ISearchEmbeddable; } -class GetCsvReportPanelAction implements Action { +declare module '../../../../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [CSV_REPORTING_ACTION]: CSVActionContext; + } +} + +class GetCsvReportPanelAction implements ActionByType { private isDownloading: boolean; public readonly type = CSV_REPORTING_ACTION; public readonly id = CSV_REPORTING_ACTION; @@ -64,13 +73,13 @@ class GetCsvReportPanelAction implements Action { return searchEmbeddable.getSavedSearch().searchSource.getSearchRequestBody(); } - public isCompatible = async (context: ActionContext) => { + public isCompatible = async (context: CSVActionContext) => { const { embeddable } = context; return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; }; - public execute = async (context: ActionContext) => { + public execute = async (context: CSVActionContext) => { const { embeddable } = context; if (!isSavedSearchEmbeddable(embeddable)) { @@ -166,4 +175,4 @@ class GetCsvReportPanelAction implements Action { const action = new GetCsvReportPanelAction(); npSetup.plugins.uiActions.registerAction(action); -npSetup.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action.id); +npSetup.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action); diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx index aa31b035cda5..325a5ddc1017 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { Action, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { ActionByType, 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'; -const CUSTOM_TIME_RANGE = 'CUSTOM_TIME_RANGE'; +export const CUSTOM_TIME_RANGE = 'CUSTOM_TIME_RANGE'; const SEARCH_EMBEDDABLE_TYPE = 'search'; export interface TimeRangeInput extends EmbeddableInput { @@ -34,11 +34,11 @@ function isVisualizeEmbeddable( return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE; } -interface ActionContext { +export interface TimeRangeActionContext { embeddable: Embeddable; } -export class CustomTimeRangeAction implements Action { +export class CustomTimeRangeAction implements ActionByType { public readonly type = CUSTOM_TIME_RANGE; private openModal: OpenModal; private dateFormat?: string; @@ -70,7 +70,7 @@ export class CustomTimeRangeAction implements Action { return 'calendar'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible({ embeddable }: TimeRangeActionContext) { const isInputControl = isVisualizeEmbeddable(embeddable) && (embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'input_control_vis'; @@ -89,7 +89,7 @@ export class CustomTimeRangeAction implements Action { ); } - public async execute({ embeddable }: ActionContext) { + public async execute({ embeddable }: TimeRangeActionContext) { const isCompatible = await this.isCompatible({ embeddable }); if (!isCompatible) { throw new IncompatibleActionError(); diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx index 4ee8c91ff2a3..59a2fc27267b 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { prettyDuration, commonDurationRanges } from '@elastic/eui'; import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { Action, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { ActionByType, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { doesInheritTimeRange } from './does_inherit_time_range'; import { OpenModal, CommonlyUsedRange } from './types'; -const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE'; +export const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE'; export interface TimeRangeInput extends EmbeddableInput { timeRange: TimeRange; @@ -25,11 +25,11 @@ function hasTimeRange( return (embeddable as Embeddable).getInput().timeRange !== undefined; } -interface ActionContext { +export interface TimeBadgeActionContext { embeddable: Embeddable; } -export class CustomTimeRangeBadge implements Action { +export class CustomTimeRangeBadge implements ActionByType { public readonly type = CUSTOM_TIME_RANGE_BADGE; public readonly id = CUSTOM_TIME_RANGE_BADGE; public order = 7; @@ -51,7 +51,7 @@ export class CustomTimeRangeBadge implements Action { this.commonlyUsedRanges = commonlyUsedRanges; } - public getDisplayName({ embeddable }: ActionContext) { + public getDisplayName({ embeddable }: TimeBadgeActionContext) { return prettyDuration( embeddable.getInput().timeRange.from, embeddable.getInput().timeRange.to, @@ -64,11 +64,11 @@ export class CustomTimeRangeBadge implements Action { return 'calendar'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible({ embeddable }: TimeBadgeActionContext) { return Boolean(embeddable && hasTimeRange(embeddable) && !doesInheritTimeRange(embeddable)); } - public async execute({ embeddable }: ActionContext) { + public async execute({ embeddable }: TimeBadgeActionContext) { const isCompatible = await this.isCompatible({ embeddable }); if (!isCompatible) { throw new IncompatibleActionError(); diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index 5c5d2d38da15..2f6935cdf196 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -18,9 +18,17 @@ import { IEmbeddableSetup, IEmbeddableStart, } from '../../../../src/plugins/embeddable/public'; -import { CustomTimeRangeAction } from './custom_time_range_action'; +import { + CustomTimeRangeAction, + CUSTOM_TIME_RANGE, + TimeRangeActionContext, +} from './custom_time_range_action'; -import { CustomTimeRangeBadge } from './custom_time_range_badge'; +import { + CustomTimeRangeBadge, + CUSTOM_TIME_RANGE_BADGE, + TimeBadgeActionContext, +} from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; interface SetupDependencies { @@ -36,6 +44,13 @@ interface StartDependencies { export type Setup = void; export type Start = void; +declare module '../../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [CUSTOM_TIME_RANGE]: TimeRangeActionContext; + [CUSTOM_TIME_RANGE_BADGE]: TimeBadgeActionContext; + } +} + export class AdvancedUiActionsPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} @@ -52,7 +67,7 @@ export class AdvancedUiActionsPublicPlugin commonlyUsedRanges, }); uiActions.registerAction(timeRangeAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, timeRangeAction.id); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, timeRangeAction); const timeRangeBadge = new CustomTimeRangeBadge({ openModal, @@ -60,7 +75,7 @@ export class AdvancedUiActionsPublicPlugin commonlyUsedRanges, }); uiActions.registerAction(timeRangeBadge); - uiActions.attachAction(PANEL_BADGE_TRIGGER, timeRangeBadge.id); + uiActions.attachAction(PANEL_BADGE_TRIGGER, timeRangeBadge); } public stop() {} diff --git a/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx index 0b9f54f51f61..1db57eb3d0b2 100644 --- a/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx +++ b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'src/core/public'; -import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { ActionByType } from '../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; import { FlyoutCreateDrilldown } from '../../components/flyout_create_drilldown'; @@ -22,7 +22,7 @@ export interface OpenFlyoutAddDrilldownParams { overlays: () => Promise; } -export class FlyoutCreateDrilldownAction implements Action { +export class FlyoutCreateDrilldownAction implements ActionByType { public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; public order = 5; diff --git a/x-pack/plugins/drilldowns/public/plugin.ts b/x-pack/plugins/drilldowns/public/plugin.ts index 6c8555fa55a1..1761e17d5598 100644 --- a/x-pack/plugins/drilldowns/public/plugin.ts +++ b/x-pack/plugins/drilldowns/public/plugin.ts @@ -7,6 +7,7 @@ import { CoreStart, CoreSetup, Plugin } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { DrilldownService } from './service'; +import { FlyoutCreateDrilldownActionContext, OPEN_FLYOUT_ADD_DRILLDOWN } from './actions'; export interface DrilldownsSetupDependencies { uiActions: UiActionsSetup; @@ -21,6 +22,12 @@ export type DrilldownsSetupContract = Pick