mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Further improve type checking for actions and triggers (#58765)
* wip * review follow up * make ACTION a prefix, not SUFFIX * fix path * add warnings about casting to ActionType * Make context an object in examples, not a string * require object context, which seems to fix the partial requirement in type and thus the type issue * mistake Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
aea4811750
commit
d2cbc59ad4
62 changed files with 565 additions and 396 deletions
|
@ -22,7 +22,7 @@ import { OverlayStart } from '../../../src/core/public';
|
|||
import { createAction } from '../../../src/plugins/ui_actions/public';
|
||||
import { toMountPoint } from '../../../src/plugins/kibana_react/public';
|
||||
|
||||
export const HELLO_WORLD_ACTION_TYPE = 'HELLO_WORLD_ACTION_TYPE';
|
||||
export const ACTION_HELLO_WORLD = 'ACTION_HELLO_WORLD';
|
||||
|
||||
interface StartServices {
|
||||
openModal: OverlayStart['openModal'];
|
||||
|
@ -30,7 +30,7 @@ interface StartServices {
|
|||
|
||||
export const createHelloWorldAction = (getStartServices: () => Promise<StartServices>) =>
|
||||
createAction({
|
||||
type: HELLO_WORLD_ACTION_TYPE,
|
||||
type: ACTION_HELLO_WORLD,
|
||||
getDisplayName: () => 'Hello World!',
|
||||
execute: async () => {
|
||||
const { openModal } = await getStartServices();
|
||||
|
|
|
@ -23,4 +23,4 @@ import { PluginInitializer } from '../../../src/core/public';
|
|||
export const plugin: PluginInitializer<void, void> = () => new UiActionExamplesPlugin();
|
||||
|
||||
export { HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger';
|
||||
export { HELLO_WORLD_ACTION_TYPE } from './hello_world_action';
|
||||
export { ACTION_HELLO_WORLD } from './hello_world_action';
|
||||
|
|
|
@ -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, ACTION_HELLO_WORLD } from './hello_world_action';
|
||||
import { helloWorldTrigger, HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger';
|
||||
|
||||
interface UiActionExamplesSetupDependencies {
|
||||
|
@ -28,7 +28,11 @@ interface UiActionExamplesSetupDependencies {
|
|||
|
||||
declare module '../../../src/plugins/ui_actions/public' {
|
||||
export interface TriggerContextMapping {
|
||||
[HELLO_WORLD_TRIGGER_ID]: undefined;
|
||||
[HELLO_WORLD_TRIGGER_ID]: {};
|
||||
}
|
||||
|
||||
export interface ActionContextMapping {
|
||||
[ACTION_HELLO_WORLD]: {};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +46,7 @@ export class UiActionExamplesPlugin
|
|||
}));
|
||||
|
||||
uiActions.registerAction(helloWorldAction);
|
||||
uiActions.attachAction(helloWorldTrigger.id, helloWorldAction.id);
|
||||
uiActions.attachAction(helloWorldTrigger.id, helloWorldAction);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
|
|
@ -27,44 +27,48 @@ export const USER_TRIGGER = 'USER_TRIGGER';
|
|||
export const COUNTRY_TRIGGER = 'COUNTRY_TRIGGER';
|
||||
export const PHONE_TRIGGER = 'PHONE_TRIGGER';
|
||||
|
||||
export const VIEW_IN_MAPS_ACTION = 'VIEW_IN_MAPS_ACTION';
|
||||
export const TRAVEL_GUIDE_ACTION = 'TRAVEL_GUIDE_ACTION';
|
||||
export const CALL_PHONE_NUMBER_ACTION = 'CALL_PHONE_NUMBER_ACTION';
|
||||
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 ACTION_VIEW_IN_MAPS = 'ACTION_VIEW_IN_MAPS';
|
||||
export const ACTION_TRAVEL_GUIDE = 'ACTION_TRAVEL_GUIDE';
|
||||
export const ACTION_CALL_PHONE_NUMBER = 'ACTION_CALL_PHONE_NUMBER';
|
||||
export const ACTION_EDIT_USER = 'ACTION_EDIT_USER';
|
||||
export const ACTION_PHONE_USER = 'ACTION_PHONE_USER';
|
||||
export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY';
|
||||
|
||||
export const showcasePluggability = createAction({
|
||||
type: SHOWCASE_PLUGGABILITY_ACTION,
|
||||
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?!"),
|
||||
});
|
||||
|
||||
export type PhoneContext = string;
|
||||
export interface PhoneContext {
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export const makePhoneCallAction = createAction<PhoneContext>({
|
||||
type: CALL_PHONE_NUMBER_ACTION,
|
||||
export const makePhoneCallAction = createAction<typeof ACTION_CALL_PHONE_NUMBER>({
|
||||
type: ACTION_CALL_PHONE_NUMBER,
|
||||
getDisplayName: () => 'Call phone number',
|
||||
execute: async phone => alert(`Pretend calling ${phone}...`),
|
||||
execute: async context => alert(`Pretend calling ${context.phone}...`),
|
||||
});
|
||||
|
||||
export const lookUpWeatherAction = createAction<{ country: string }>({
|
||||
type: TRAVEL_GUIDE_ACTION,
|
||||
export const lookUpWeatherAction = createAction<typeof ACTION_TRAVEL_GUIDE>({
|
||||
type: ACTION_TRAVEL_GUIDE,
|
||||
getIconType: () => 'popout',
|
||||
getDisplayName: () => 'View travel guide',
|
||||
execute: async ({ country }) => {
|
||||
window.open(`https://www.worldtravelguide.net/?s=${country},`, '_blank');
|
||||
execute: async context => {
|
||||
window.open(`https://www.worldtravelguide.net/?s=${context.country}`, '_blank');
|
||||
},
|
||||
});
|
||||
|
||||
export type CountryContext = string;
|
||||
export interface CountryContext {
|
||||
country: string;
|
||||
}
|
||||
|
||||
export const viewInMapsAction = createAction<CountryContext>({
|
||||
type: VIEW_IN_MAPS_ACTION,
|
||||
export const viewInMapsAction = createAction<typeof ACTION_VIEW_IN_MAPS>({
|
||||
type: ACTION_VIEW_IN_MAPS,
|
||||
getIconType: () => 'popout',
|
||||
getDisplayName: () => 'View in maps',
|
||||
execute: async country => {
|
||||
window.open(`https://www.google.com/maps/place/${country}`, '_blank');
|
||||
execute: async context => {
|
||||
window.open(`https://www.google.com/maps/place/${context.country}`, '_blank');
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -100,11 +104,8 @@ function EditUserModal({
|
|||
}
|
||||
|
||||
export const createEditUserAction = (getOpenModal: () => Promise<OverlayStart['openModal']>) =>
|
||||
createAction<{
|
||||
user: User;
|
||||
update: (user: User) => void;
|
||||
}>({
|
||||
type: EDIT_USER_ACTION,
|
||||
createAction<typeof ACTION_EDIT_USER>({
|
||||
type: ACTION_EDIT_USER,
|
||||
getIconType: () => 'pencil',
|
||||
getDisplayName: () => 'Edit user',
|
||||
execute: async ({ user, update }) => {
|
||||
|
@ -120,8 +121,8 @@ export interface UserContext {
|
|||
}
|
||||
|
||||
export const createPhoneUserAction = (getUiActionsApi: () => Promise<UiActionsStart>) =>
|
||||
createAction<UserContext>({
|
||||
type: PHONE_USER_ACTION,
|
||||
createAction<typeof ACTION_PHONE_USER>({
|
||||
type: ACTION_PHONE_USER,
|
||||
getDisplayName: () => 'Call phone number',
|
||||
isCompatible: async ({ user }) => user.phone !== undefined,
|
||||
execute: async ({ user }) => {
|
||||
|
@ -133,7 +134,7 @@ export const createPhoneUserAction = (getUiActionsApi: () => Promise<UiActionsSt
|
|||
// TODO: we need to figure out the best way to handle these nested actions however, since
|
||||
// we don't want multiple context menu's to pop up.
|
||||
if (user.phone !== undefined) {
|
||||
(await getUiActionsApi()).executeTriggerActions(PHONE_TRIGGER, user.phone);
|
||||
(await getUiActionsApi()).executeTriggerActions(PHONE_TRIGGER, { phone: user.phone });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ import { EuiModalBody } from '@elastic/eui';
|
|||
import { toMountPoint } from '../../../src/plugins/kibana_react/public';
|
||||
import { UiActionsStart, createAction } from '../../../src/plugins/ui_actions/public';
|
||||
import { AppMountParameters, OverlayStart } from '../../../src/core/public';
|
||||
import { HELLO_WORLD_TRIGGER_ID, HELLO_WORLD_ACTION_TYPE } from '../../ui_action_examples/public';
|
||||
import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '../../ui_action_examples/public';
|
||||
import { TriggerContextExample } from './trigger_context_example';
|
||||
|
||||
interface Props {
|
||||
|
@ -60,7 +60,7 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => {
|
|||
</EuiText>
|
||||
<EuiButton
|
||||
data-test-subj="emitHelloWorldTrigger"
|
||||
onClick={() => uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, undefined)}
|
||||
onClick={() => uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})}
|
||||
>
|
||||
Say hello world!
|
||||
</EuiButton>
|
||||
|
@ -76,8 +76,9 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => {
|
|||
<EuiButton
|
||||
data-test-subj="addDynamicAction"
|
||||
onClick={() => {
|
||||
const dynamicAction = createAction<{}>({
|
||||
type: `${HELLO_WORLD_ACTION_TYPE}-${name}`,
|
||||
const dynamicAction = createAction<typeof ACTION_HELLO_WORLD>({
|
||||
id: `${ACTION_HELLO_WORLD}-${name}`,
|
||||
type: ACTION_HELLO_WORLD,
|
||||
getDisplayName: () => `Say hello to ${name}`,
|
||||
execute: async () => {
|
||||
const overlay = openModal(
|
||||
|
@ -95,7 +96,7 @@ 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(
|
||||
{}
|
||||
|
|
|
@ -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,
|
||||
ACTION_EDIT_USER,
|
||||
ACTION_SHOWCASE_PLUGGABILITY,
|
||||
ACTION_CALL_PHONE_NUMBER,
|
||||
ACTION_TRAVEL_GUIDE,
|
||||
ACTION_VIEW_IN_MAPS,
|
||||
ACTION_PHONE_USER,
|
||||
} 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 {
|
||||
[ACTION_EDIT_USER]: UserContext;
|
||||
[ACTION_SHOWCASE_PLUGGABILITY]: {};
|
||||
[ACTION_CALL_PHONE_NUMBER]: PhoneContext;
|
||||
[ACTION_TRAVEL_GUIDE]: CountryContext;
|
||||
[ACTION_VIEW_IN_MAPS]: CountryContext;
|
||||
[ACTION_PHONE_USER]: UserContext;
|
||||
}
|
||||
}
|
||||
|
||||
export class UiActionsExplorerPlugin implements Plugin<void, void, {}, StartDeps> {
|
||||
|
@ -67,29 +76,24 @@ export class UiActionsExplorerPlugin implements Plugin<void, void, {}, StartDeps
|
|||
deps.uiActions.registerTrigger({
|
||||
id: USER_TRIGGER,
|
||||
});
|
||||
deps.uiActions.registerAction(lookUpWeatherAction);
|
||||
deps.uiActions.registerAction(viewInMapsAction);
|
||||
deps.uiActions.registerAction(makePhoneCallAction);
|
||||
deps.uiActions.registerAction(showcasePluggability);
|
||||
|
||||
const startServices = core.getStartServices();
|
||||
deps.uiActions.registerAction(
|
||||
|
||||
deps.uiActions.attachAction(
|
||||
USER_TRIGGER,
|
||||
createPhoneUserAction(async () => (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',
|
||||
|
|
|
@ -47,7 +47,7 @@ const createRowData = (
|
|||
<Fragment>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, user.countryOfResidence);
|
||||
uiActionsApi.executeTriggerActions(COUNTRY_TRIGGER, { country: user.countryOfResidence });
|
||||
}}
|
||||
>
|
||||
{user.countryOfResidence}
|
||||
|
@ -59,7 +59,7 @@ const createRowData = (
|
|||
<EuiButtonEmpty
|
||||
disabled={user.phone === undefined}
|
||||
onClick={() => {
|
||||
uiActionsApi.executeTriggerActions(PHONE_TRIGGER, user.phone!);
|
||||
uiActionsApi.executeTriggerActions(PHONE_TRIGGER, { phone: user.phone! });
|
||||
}}
|
||||
>
|
||||
{user.phone}
|
||||
|
|
|
@ -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';
|
||||
export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE';
|
||||
|
||||
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,17 +44,17 @@ async function isCompatible(context: ActionContext) {
|
|||
export function selectRangeAction(
|
||||
filterManager: FilterManager,
|
||||
timeFilter: TimefilterContract
|
||||
): Action<ActionContext> {
|
||||
return createAction<ActionContext>({
|
||||
type: SELECT_RANGE_ACTION,
|
||||
id: SELECT_RANGE_ACTION,
|
||||
): ActionByType<typeof ACTION_SELECT_RANGE> {
|
||||
return createAction<typeof ACTION_SELECT_RANGE>({
|
||||
type: ACTION_SELECT_RANGE,
|
||||
id: ACTION_SELECT_RANGE,
|
||||
getDisplayName: () => {
|
||||
return i18n.translate('data.filter.applyFilterActionTitle', {
|
||||
defaultMessage: 'Apply filter to current view',
|
||||
});
|
||||
},
|
||||
isCompatible,
|
||||
execute: async ({ timeFieldName, data }: ActionContext) => {
|
||||
execute: async ({ timeFieldName, data }: SelectRangeActionContext) => {
|
||||
if (!(await isCompatible({ timeFieldName, data }))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -37,14 +37,14 @@ import {
|
|||
esFilters,
|
||||
} from '../../../../../plugins/data/public';
|
||||
|
||||
export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION';
|
||||
export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK';
|
||||
|
||||
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.data || [context.data], context.data.negate)) ||
|
||||
|
@ -58,17 +58,17 @@ async function isCompatible(context: ActionContext) {
|
|||
export function valueClickAction(
|
||||
filterManager: FilterManager,
|
||||
timeFilter: TimefilterContract
|
||||
): Action<ActionContext> {
|
||||
return createAction<ActionContext>({
|
||||
type: VALUE_CLICK_ACTION,
|
||||
id: VALUE_CLICK_ACTION,
|
||||
): ActionByType<typeof ACTION_VALUE_CLICK> {
|
||||
return createAction<typeof ACTION_VALUE_CLICK>({
|
||||
type: ACTION_VALUE_CLICK,
|
||||
id: ACTION_VALUE_CLICK,
|
||||
getDisplayName: () => {
|
||||
return i18n.translate('data.filter.applyFilterActionTitle', {
|
||||
defaultMessage: 'Apply filter to current view',
|
||||
});
|
||||
},
|
||||
isCompatible,
|
||||
execute: async ({ timeFieldName, data }: ActionContext) => {
|
||||
execute: async ({ timeFieldName, data }: ValueClickActionContext) => {
|
||||
if (!(await isCompatible({ timeFieldName, data }))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
ACTION_SELECT_RANGE,
|
||||
} from './actions/select_range_action';
|
||||
import {
|
||||
valueClickAction,
|
||||
ACTION_VALUE_CLICK,
|
||||
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 {
|
||||
[ACTION_SELECT_RANGE]: SelectRangeActionContext;
|
||||
[ACTION_VALUE_CLICK]: 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,
|
||||
};
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
|
||||
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';
|
||||
export const ACTION_EXPAND_PANEL = 'togglePanel';
|
||||
|
||||
function isDashboard(embeddable: IEmbeddable): embeddable is DashboardContainer {
|
||||
return embeddable.type === DASHBOARD_CONTAINER_TYPE;
|
||||
|
@ -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<ActionContext> {
|
||||
public readonly type = EXPAND_PANEL_ACTION;
|
||||
public readonly id = EXPAND_PANEL_ACTION;
|
||||
export class ExpandPanelAction implements ActionByType<typeof ACTION_EXPAND_PANEL> {
|
||||
public readonly type = ACTION_EXPAND_PANEL;
|
||||
public readonly id = ACTION_EXPAND_PANEL;
|
||||
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<ActionContext> {
|
|||
);
|
||||
}
|
||||
|
||||
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<ActionContext> {
|
|||
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();
|
||||
}
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { ExpandPanelAction, EXPAND_PANEL_ACTION } from './expand_panel_action';
|
||||
export { ReplacePanelAction, REPLACE_PANEL_ACTION } from './replace_panel_action';
|
||||
export { ExpandPanelAction, ACTION_EXPAND_PANEL } from './expand_panel_action';
|
||||
export { ReplacePanelAction, ACTION_REPLACE_PANEL } from './replace_panel_action';
|
||||
|
|
|
@ -21,22 +21,22 @@ 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';
|
||||
export const ACTION_REPLACE_PANEL = 'replacePanel';
|
||||
|
||||
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<ActionContext> {
|
||||
public readonly type = REPLACE_PANEL_ACTION;
|
||||
public readonly id = REPLACE_PANEL_ACTION;
|
||||
export class ReplacePanelAction implements ActionByType<typeof ACTION_REPLACE_PANEL> {
|
||||
public readonly type = ACTION_REPLACE_PANEL;
|
||||
public readonly id = ACTION_REPLACE_PANEL;
|
||||
public order = 11;
|
||||
|
||||
constructor(
|
||||
|
@ -46,7 +46,7 @@ export class ReplacePanelAction implements Action<ActionContext> {
|
|||
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<ActionContext> {
|
|||
});
|
||||
}
|
||||
|
||||
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<ActionContext> {
|
|||
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();
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ import {
|
|||
ExitFullScreenButton as ExitFullScreenButtonUi,
|
||||
ExitFullScreenButtonProps,
|
||||
} from '../../../plugins/kibana_react/public';
|
||||
import { ExpandPanelActionContext, ACTION_EXPAND_PANEL } from './actions/expand_panel_action';
|
||||
import { ReplacePanelActionContext, ACTION_REPLACE_PANEL } 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 {
|
||||
[ACTION_EXPAND_PANEL]: ExpandPanelActionContext;
|
||||
[ACTION_REPLACE_PANEL]: ReplacePanelActionContext;
|
||||
}
|
||||
}
|
||||
|
||||
export class DashboardEmbeddableContainerPublicPlugin
|
||||
implements Plugin<Setup, Start, SetupDependencies, StartDependencies> {
|
||||
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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -19,36 +19,36 @@
|
|||
|
||||
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';
|
||||
export const ACTION_GLOBAL_APPLY_FILTER = 'ACTION_GLOBAL_APPLY_FILTER';
|
||||
|
||||
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<ActionContext> {
|
||||
return createAction<ActionContext>({
|
||||
type: GLOBAL_APPLY_FILTER_ACTION,
|
||||
id: GLOBAL_APPLY_FILTER_ACTION,
|
||||
): ActionByType<typeof ACTION_GLOBAL_APPLY_FILTER> {
|
||||
return createAction<typeof ACTION_GLOBAL_APPLY_FILTER>({
|
||||
type: ACTION_GLOBAL_APPLY_FILTER,
|
||||
id: ACTION_GLOBAL_APPLY_FILTER,
|
||||
getDisplayName: () => {
|
||||
return i18n.translate('data.filter.applyFilterActionTitle', {
|
||||
defaultMessage: 'Apply filter to current view',
|
||||
});
|
||||
},
|
||||
isCompatible,
|
||||
execute: async ({ filters, timeFieldName }: ActionContext) => {
|
||||
execute: async ({ filters, timeFieldName }: ApplyGlobalFilterActionContext) => {
|
||||
if (!filters) {
|
||||
throw new Error('Applying a filter requires a filter');
|
||||
}
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { GLOBAL_APPLY_FILTER_ACTION, createFilterAction } from './apply_filter_action';
|
||||
export { ACTION_GLOBAL_APPLY_FILTER, createFilterAction } from './apply_filter_action';
|
||||
|
|
|
@ -44,9 +44,16 @@ import {
|
|||
setIndexPatterns,
|
||||
setUiSettings,
|
||||
} from './services';
|
||||
import { createFilterAction, GLOBAL_APPLY_FILTER_ACTION } from './actions';
|
||||
import { createFilterAction, ACTION_GLOBAL_APPLY_FILTER } 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 {
|
||||
[ACTION_GLOBAL_APPLY_FILTER]: ApplyGlobalFilterActionContext;
|
||||
}
|
||||
}
|
||||
|
||||
export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPublicPluginStart> {
|
||||
private readonly autocomplete = new AutocompleteService();
|
||||
|
@ -93,7 +100,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
|
|||
const indexPatternsService = new IndexPatternsService(uiSettings, savedObjects.client, http);
|
||||
setIndexPatterns(indexPatternsService);
|
||||
|
||||
uiActions.attachAction(APPLY_FILTER_TRIGGER, GLOBAL_APPLY_FILTER_ACTION);
|
||||
uiActions.attachAction(APPLY_FILTER_TRIGGER, uiActions.getAction(ACTION_GLOBAL_APPLY_FILTER));
|
||||
|
||||
const dataServices = {
|
||||
autocomplete: this.autocomplete.start(),
|
||||
|
|
|
@ -33,6 +33,13 @@ import {
|
|||
SELECT_RANGE_TRIGGER,
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
PANEL_BADGE_TRIGGER,
|
||||
ACTION_ADD_PANEL,
|
||||
ACTION_CUSTOMIZE_PANEL,
|
||||
ACTION_INSPECT_PANEL,
|
||||
REMOVE_PANEL_ACTION,
|
||||
ACTION_EDIT_PANEL,
|
||||
FilterActionContext,
|
||||
ACTION_APPLY_FILTER,
|
||||
} from './lib';
|
||||
|
||||
declare module '../../ui_actions/public' {
|
||||
|
@ -46,6 +53,15 @@ declare module '../../ui_actions/public' {
|
|||
[CONTEXT_MENU_TRIGGER]: EmbeddableContext;
|
||||
[PANEL_BADGE_TRIGGER]: EmbeddableContext;
|
||||
}
|
||||
|
||||
export interface ActionContextMapping {
|
||||
[ACTION_CUSTOMIZE_PANEL]: EmbeddableContext;
|
||||
[ACTION_ADD_PANEL]: EmbeddableContext;
|
||||
[ACTION_INSPECT_PANEL]: EmbeddableContext;
|
||||
[REMOVE_PANEL_ACTION]: EmbeddableContext;
|
||||
[ACTION_EDIT_PANEL]: EmbeddableContext;
|
||||
[ACTION_APPLY_FILTER]: FilterActionContext;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,9 +24,9 @@ import { EmbeddablePublicPlugin } from './plugin';
|
|||
|
||||
export {
|
||||
Adapters,
|
||||
ADD_PANEL_ACTION_ID,
|
||||
ACTION_ADD_PANEL,
|
||||
AddPanelAction,
|
||||
APPLY_FILTER_ACTION,
|
||||
ACTION_APPLY_FILTER,
|
||||
APPLY_FILTER_TRIGGER,
|
||||
applyFilterTrigger,
|
||||
Container,
|
||||
|
@ -34,7 +34,7 @@ export {
|
|||
ContainerOutput,
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
contextMenuTrigger,
|
||||
EDIT_PANEL_ACTION_ID,
|
||||
ACTION_EDIT_PANEL,
|
||||
EditPanelAction,
|
||||
Embeddable,
|
||||
EmbeddableChildPanel,
|
||||
|
|
|
@ -20,10 +20,10 @@
|
|||
import { createFilterAction } from './apply_filter_action';
|
||||
import { expectErrorAsync } from '../../tests/helpers';
|
||||
|
||||
test('has APPLY_FILTER_ACTION type and id', () => {
|
||||
test('has ACTION_APPLY_FILTER type and id', () => {
|
||||
const action = createFilterAction();
|
||||
expect(action.id).toBe('APPLY_FILTER_ACTION');
|
||||
expect(action.type).toBe('APPLY_FILTER_ACTION');
|
||||
expect(action.id).toBe('ACTION_APPLY_FILTER');
|
||||
expect(action.type).toBe('ACTION_APPLY_FILTER');
|
||||
});
|
||||
|
||||
test('has expected display name', () => {
|
||||
|
|
|
@ -18,19 +18,19 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Action, createAction, IncompatibleActionError } from '../ui_actions';
|
||||
import { ActionByType, createAction, IncompatibleActionError } from '../ui_actions';
|
||||
import { IEmbeddable, EmbeddableInput } from '../embeddables';
|
||||
import { Filter } from '../../../../../plugins/data/public';
|
||||
|
||||
export const APPLY_FILTER_ACTION = 'APPLY_FILTER_ACTION';
|
||||
export const ACTION_APPLY_FILTER = 'ACTION_APPLY_FILTER';
|
||||
|
||||
type RootEmbeddable = IEmbeddable<EmbeddableInput & { filters: Filter[] }>;
|
||||
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,10 +38,10 @@ async function isCompatible(context: ActionContext) {
|
|||
return Boolean(root.getInput().filters !== undefined && context.filters !== undefined);
|
||||
}
|
||||
|
||||
export function createFilterAction(): Action<ActionContext> {
|
||||
return createAction<ActionContext>({
|
||||
type: APPLY_FILTER_ACTION,
|
||||
id: APPLY_FILTER_ACTION,
|
||||
export function createFilterAction(): ActionByType<typeof ACTION_APPLY_FILTER> {
|
||||
return createAction<typeof ACTION_APPLY_FILTER>({
|
||||
type: ACTION_APPLY_FILTER,
|
||||
id: ACTION_APPLY_FILTER,
|
||||
getDisplayName: () => {
|
||||
return i18n.translate('embeddableApi.actions.applyFilterActionTitle', {
|
||||
defaultMessage: 'Apply filter to current view',
|
||||
|
|
|
@ -23,15 +23,15 @@ import { GetEmbeddableFactory, ViewMode } from '../types';
|
|||
import { EmbeddableFactoryNotFoundError } from '../errors';
|
||||
import { IEmbeddable } from '../embeddables';
|
||||
|
||||
export const EDIT_PANEL_ACTION_ID = 'editPanel';
|
||||
export const ACTION_EDIT_PANEL = 'editPanel';
|
||||
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class EditPanelAction implements Action<ActionContext> {
|
||||
public readonly type = EDIT_PANEL_ACTION_ID;
|
||||
public readonly id = EDIT_PANEL_ACTION_ID;
|
||||
public readonly type = ACTION_EDIT_PANEL;
|
||||
public readonly id = ACTION_EDIT_PANEL;
|
||||
public order = 15;
|
||||
|
||||
constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -17,9 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
ADD_PANEL_ACTION_ID,
|
||||
AddPanelAction,
|
||||
RemovePanelAction,
|
||||
openAddPanelFlyout,
|
||||
} from './panel_actions';
|
||||
export * from './panel_actions';
|
||||
|
|
|
@ -23,15 +23,15 @@ import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../..
|
|||
import { openAddPanelFlyout } from './open_add_panel_flyout';
|
||||
import { IContainer } from '../../../../containers';
|
||||
|
||||
export const ADD_PANEL_ACTION_ID = 'ADD_PANEL_ACTION_ID';
|
||||
export const ACTION_ADD_PANEL = 'ACTION_ADD_PANEL';
|
||||
|
||||
interface ActionContext {
|
||||
embeddable: IContainer;
|
||||
}
|
||||
|
||||
export class AddPanelAction implements Action<ActionContext> {
|
||||
public readonly type = ADD_PANEL_ACTION_ID;
|
||||
public readonly id = ADD_PANEL_ACTION_ID;
|
||||
public readonly type = ACTION_ADD_PANEL;
|
||||
public readonly id = ACTION_ADD_PANEL;
|
||||
|
||||
constructor(
|
||||
private readonly getFactory: GetEmbeddableFactory,
|
||||
|
|
|
@ -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 ACTION_CUSTOMIZE_PANEL = 'ACTION_CUSTOMIZE_PANEL';
|
||||
|
||||
type GetUserData = (context: ActionContext) => Promise<{ title: string | undefined }>;
|
||||
|
||||
|
@ -31,8 +31,8 @@ interface ActionContext {
|
|||
}
|
||||
|
||||
export class CustomizePanelTitleAction implements Action<ActionContext> {
|
||||
public readonly type = CUSTOMIZE_PANEL_ACTION_ID;
|
||||
public id = CUSTOMIZE_PANEL_ACTION_ID;
|
||||
public readonly type = ACTION_CUSTOMIZE_PANEL;
|
||||
public id = ACTION_CUSTOMIZE_PANEL;
|
||||
public order = 10;
|
||||
|
||||
constructor(private readonly getDataFromUser: GetUserData) {
|
||||
|
|
|
@ -17,14 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Action, createAction } from '../../actions';
|
||||
|
||||
export const RESTRICTED_ACTION = 'RESTRICTED_ACTION';
|
||||
|
||||
export function createRestrictedAction<C>(isCompatibleIn: (context: C) => boolean): Action<C> {
|
||||
return createAction<C>({
|
||||
type: RESTRICTED_ACTION,
|
||||
isCompatible: async context => isCompatibleIn(context),
|
||||
execute: async () => {},
|
||||
});
|
||||
}
|
||||
export * from './customize_panel_action';
|
|
@ -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';
|
||||
|
|
|
@ -22,15 +22,15 @@ import { Action } from 'src/plugins/ui_actions/public';
|
|||
import { Start as InspectorStartContract } from 'src/plugins/inspector/public';
|
||||
import { IEmbeddable } from '../../../embeddables';
|
||||
|
||||
export const INSPECT_PANEL_ACTION_ID = 'openInspector';
|
||||
export const ACTION_INSPECT_PANEL = 'openInspector';
|
||||
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class InspectPanelAction implements Action<ActionContext> {
|
||||
public readonly type = INSPECT_PANEL_ACTION_ID;
|
||||
public readonly id = INSPECT_PANEL_ACTION_ID;
|
||||
public readonly type = ACTION_INSPECT_PANEL;
|
||||
public readonly id = ACTION_INSPECT_PANEL;
|
||||
public order = 10;
|
||||
|
||||
constructor(private readonly inspector: InspectorStartContract) {}
|
||||
|
|
|
@ -17,17 +17,20 @@
|
|||
* 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';
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION' as ActionType;
|
||||
|
||||
export function createEditModeAction() {
|
||||
return createAction<EmbeddableContext>({
|
||||
return createAction<typeof EDIT_MODE_ACTION>({
|
||||
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 () => {},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
* 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';
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION' as ActionType;
|
||||
|
||||
export interface FullNameEmbeddableOutput extends EmbeddableOutput {
|
||||
fullName: string;
|
||||
|
@ -35,12 +37,12 @@ export function hasFullNameOutput(
|
|||
);
|
||||
}
|
||||
|
||||
interface ActionContext {
|
||||
export interface SayHelloActionContext {
|
||||
embeddable: Embeddable<EmbeddableInput, FullNameEmbeddableOutput>;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class SayHelloAction implements Action<ActionContext> {
|
||||
export class SayHelloAction implements ActionByType<typeof SAY_HELLO_ACTION> {
|
||||
public readonly type = SAY_HELLO_ACTION;
|
||||
public readonly id = SAY_HELLO_ACTION;
|
||||
|
||||
|
@ -62,7 +64,7 @@ export class SayHelloAction implements Action<ActionContext> {
|
|||
|
||||
// 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 +72,7 @@ export class SayHelloAction implements Action<ActionContext> {
|
|||
return hasFullNameOutput(context.embeddable);
|
||||
}
|
||||
|
||||
async execute(context: ActionContext) {
|
||||
async execute(context: SayHelloActionContext) {
|
||||
if (!(await this.isCompatible(context))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
|
|
@ -18,14 +18,16 @@
|
|||
*/
|
||||
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';
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
export const ACTION_SEND_MESSAGE = 'ACTION_SEND_MESSAGE' as ActionType;
|
||||
|
||||
interface ActionContext {
|
||||
embeddable: Embeddable<EmbeddableInput, FullNameEmbeddableOutput>;
|
||||
|
@ -42,11 +44,11 @@ export function createSendMessageAction(overlays: CoreStart['overlays']) {
|
|||
overlays.openFlyout(toMountPoint(<EuiFlyoutBody>{content}</EuiFlyoutBody>));
|
||||
};
|
||||
|
||||
return createAction<ActionContext>({
|
||||
type: SEND_MESSAGE_ACTION,
|
||||
return createAction<typeof ACTION_SEND_MESSAGE>({
|
||||
type: ACTION_SEND_MESSAGE,
|
||||
getDisplayName: () => 'Send message',
|
||||
isCompatible,
|
||||
execute: async context => {
|
||||
execute: async (context: ActionContext) => {
|
||||
if (!(await isCompatible(context))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
|
|
@ -17,17 +17,23 @@
|
|||
* 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({
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
type: 'test' as ActionType,
|
||||
isCompatible: ({ amICompatible }: { amICompatible: boolean }) => Promise.resolve(amICompatible),
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -18,17 +18,26 @@
|
|||
*/
|
||||
|
||||
import { UiComponent } from 'src/plugins/kibana_utils/common';
|
||||
import { ActionType, ActionContextMapping } from '../types';
|
||||
|
||||
export interface Action<Context = undefined> {
|
||||
export type ActionByType<T extends ActionType> = Action<ActionContextMapping[T], T>;
|
||||
|
||||
export interface Action<Context = {}, T = ActionType> {
|
||||
/**
|
||||
* 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.
|
||||
|
|
72
src/plugins/ui_actions/public/actions/action_definition.ts
Normal file
72
src/plugins/ui_actions/public/actions/action_definition.ts
Normal file
|
@ -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<T extends ActionType> {
|
||||
/**
|
||||
* 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<boolean>;
|
||||
|
||||
/**
|
||||
* 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<void>;
|
||||
}
|
|
@ -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<Context = undefined>(
|
||||
action: { type: string; execute: Action<Context>['execute'] } & Partial<Action<Context>>
|
||||
): Action<Context> {
|
||||
export function createAction<T extends ActionType>(action: ActionDefinition<T>): ActionByType<T> {
|
||||
return {
|
||||
getIconType: () => undefined,
|
||||
order: 0,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -18,14 +18,13 @@
|
|||
*/
|
||||
|
||||
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
|
||||
// that seems to overwrite all other plugins extending it, I suspect because it's inside
|
||||
// the main plugin.
|
||||
// Casting to ActionType or TriggerId is a hack - in a real situation use
|
||||
// declare module and add this id to the appropriate context mapping.
|
||||
const FOO_TRIGGER: TriggerId = 'FOO_TRIGGER' as TriggerId;
|
||||
const BAR_TRIGGER: TriggerId = 'BAR_TRIGGER' as TriggerId;
|
||||
const MY_TRIGGER: TriggerId = 'MY_TRIGGER' as TriggerId;
|
||||
|
@ -33,7 +32,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 +42,7 @@ const testAction1: Action = {
|
|||
const testAction2: Action = {
|
||||
id: 'action2',
|
||||
order: 2,
|
||||
type: 'type2',
|
||||
type: 'type2' as ActionType,
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test2',
|
||||
getIconType: () => '',
|
||||
|
@ -100,7 +99,7 @@ describe('UiActionsService', () => {
|
|||
getDisplayName: () => 'test',
|
||||
getIconType: () => '',
|
||||
isCompatible: async () => true,
|
||||
type: 'test',
|
||||
type: 'test' as ActionType,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -109,7 +108,7 @@ describe('UiActionsService', () => {
|
|||
const action1: Action = {
|
||||
id: 'action1',
|
||||
order: 1,
|
||||
type: 'type1',
|
||||
type: 'type1' as ActionType,
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test',
|
||||
getIconType: () => '',
|
||||
|
@ -118,7 +117,7 @@ describe('UiActionsService', () => {
|
|||
const action2: Action = {
|
||||
id: 'action2',
|
||||
order: 2,
|
||||
type: 'type2',
|
||||
type: 'type2' as ActionType,
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test',
|
||||
getIconType: () => '',
|
||||
|
@ -140,13 +139,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 +178,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 +190,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 +204,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 +288,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 +309,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 +330,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);
|
||||
|
@ -344,7 +345,7 @@ describe('UiActionsService', () => {
|
|||
});
|
||||
|
||||
describe('registries', () => {
|
||||
const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
|
||||
const ACTION_HELLO_WORLD = 'ACTION_HELLO_WORLD';
|
||||
|
||||
test('can register trigger', () => {
|
||||
const triggers: TriggerRegistry = new Map();
|
||||
|
@ -369,12 +370,12 @@ describe('UiActionsService', () => {
|
|||
const service = new UiActionsService({ actions });
|
||||
|
||||
service.registerAction({
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
id: ACTION_HELLO_WORLD,
|
||||
order: 13,
|
||||
} as any);
|
||||
|
||||
expect(actions.get(HELLO_WORLD_ACTION_ID)).toMatchObject({
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
expect(actions.get(ACTION_HELLO_WORLD)).toMatchObject({
|
||||
id: ACTION_HELLO_WORLD,
|
||||
order: 13,
|
||||
});
|
||||
});
|
||||
|
@ -386,18 +387,17 @@ describe('UiActionsService', () => {
|
|||
id: MY_TRIGGER,
|
||||
};
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
id: ACTION_HELLO_WORLD,
|
||||
order: 25,
|
||||
} 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);
|
||||
|
||||
expect(actions.length).toBe(1);
|
||||
expect(actions[0].id).toBe(HELLO_WORLD_ACTION_ID);
|
||||
expect(actions[0].id).toBe(ACTION_HELLO_WORLD);
|
||||
});
|
||||
|
||||
test('can detach an action to a trigger', () => {
|
||||
|
@ -407,14 +407,14 @@ describe('UiActionsService', () => {
|
|||
id: MY_TRIGGER,
|
||||
};
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
id: ACTION_HELLO_WORLD,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
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([]);
|
||||
|
@ -424,15 +424,15 @@ describe('UiActionsService', () => {
|
|||
const service = new UiActionsService();
|
||||
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
id: ACTION_HELLO_WORLD,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerAction(action);
|
||||
expect(() =>
|
||||
service.detachAction('i do not exist' as TriggerId, HELLO_WORLD_ACTION_ID)
|
||||
service.detachAction('i do not exist' as TriggerId, ACTION_HELLO_WORLD)
|
||||
).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for detaching action [actionId = HELLO_WORLD_ACTION_ID].'
|
||||
'No trigger [triggerId = i do not exist] exists, for detaching action [actionId = ACTION_HELLO_WORLD].'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -440,15 +440,13 @@ describe('UiActionsService', () => {
|
|||
const service = new UiActionsService();
|
||||
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
id: ACTION_HELLO_WORLD,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerAction(action);
|
||||
expect(() =>
|
||||
service.attachAction('i do not exist' as TriggerId, HELLO_WORLD_ACTION_ID)
|
||||
).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = HELLO_WORLD_ACTION_ID].'
|
||||
expect(() => service.attachAction('i do not exist' as TriggerId, action)).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = ACTION_HELLO_WORLD].'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -456,13 +454,13 @@ describe('UiActionsService', () => {
|
|||
const service = new UiActionsService();
|
||||
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
id: ACTION_HELLO_WORLD,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerAction(action);
|
||||
expect(() => service.registerAction(action)).toThrowError(
|
||||
'Action [action.id = HELLO_WORLD_ACTION_ID] already registered.'
|
||||
'Action [action.id = ACTION_HELLO_WORLD] already registered.'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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 = <Context>(action: Action<Context>) => {
|
||||
public readonly registerAction = <T extends ActionType>(action: ActionByType<T>) => {
|
||||
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
|
||||
// <T extends TriggerId>(triggerId: T, action: Action<TriggerContextMapping[T]>): \
|
||||
// to get type checks here!
|
||||
public readonly attachAction = <T extends TriggerId>(triggerId: T, actionId: string): void => {
|
||||
public readonly getAction = <T extends ActionType>(id: string): ActionByType<T> => {
|
||||
if (!this.actions.has(id)) {
|
||||
throw new Error(`Action [action.id = ${id}] not registered.`);
|
||||
}
|
||||
|
||||
return this.actions.get(id) as ActionByType<T>;
|
||||
};
|
||||
|
||||
public readonly attachAction = <TType extends TriggerId, AType extends ActionType>(
|
||||
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: ActionByType<AType> & Action<TriggerContextMapping[TType]>
|
||||
): 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]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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,18 @@ const openContextMenuSpy = (openContextMenu as any) as jest.SpyInstance;
|
|||
|
||||
const CONTACT_USER_TRIGGER = 'CONTACT_USER_TRIGGER';
|
||||
|
||||
function createTestAction<A>(id: string, checkCompatibility: (context: A) => boolean): Action<A> {
|
||||
return createAction<A>({
|
||||
type: 'testAction',
|
||||
id,
|
||||
isCompatible: context => Promise.resolve(checkCompatibility(context)),
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
const TEST_ACTION_TYPE = 'TEST_ACTION_TYPE' as ActionType;
|
||||
|
||||
function createTestAction<C extends object>(
|
||||
type: string,
|
||||
checkCompatibility: (context: C) => boolean
|
||||
): Action<object> {
|
||||
return createAction<typeof TEST_ACTION_TYPE>({
|
||||
type: type as ActionType,
|
||||
id: type,
|
||||
isCompatible: (context: C) => Promise.resolve(checkCompatibility(context)),
|
||||
execute: context => executeFn(context),
|
||||
});
|
||||
}
|
||||
|
@ -46,7 +53,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, 'ACTION_SEND_MESSAGE');
|
||||
|
||||
executeFn.mockReset();
|
||||
openContextMenuSpy.mockReset();
|
||||
|
@ -62,8 +69,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 +87,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 +103,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 +130,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 +155,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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<typeof uiActionsPluginMock.createPlugin>;
|
||||
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 });
|
||||
|
|
|
@ -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,11 +37,15 @@ const ReactMenuItem: React.FC = () => {
|
|||
|
||||
const UiMenuItem = reactToUiComponent(ReactMenuItem);
|
||||
|
||||
export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
export const ACTION_HELLO_WORLD = 'ACTION_HELLO_WORLD' as ActionType;
|
||||
|
||||
export function createHelloWorldAction(overlays: CoreStart['overlays']): Action {
|
||||
return createAction({
|
||||
type: HELLO_WORLD_ACTION_ID,
|
||||
export function createHelloWorldAction(
|
||||
overlays: CoreStart['overlays']
|
||||
): ActionByType<typeof ACTION_HELLO_WORLD> {
|
||||
return createAction<typeof ACTION_HELLO_WORLD>({
|
||||
type: ACTION_HELLO_WORLD,
|
||||
getIconType: () => 'lock',
|
||||
MenuItem: UiMenuItem,
|
||||
execute: async () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
|
||||
this.getDisplayName(context)
|
||||
</EuiFlyout>
|
||||
),
|
||||
{
|
||||
'data-test-subj': 'sayHelloAction',
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -17,20 +17,27 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Action } from './actions/action';
|
||||
import { ActionByType } from './actions/action';
|
||||
import { TriggerInternal } from './triggers/trigger_internal';
|
||||
|
||||
export type TriggerRegistry = Map<TriggerId, TriggerInternal<any>>;
|
||||
export type ActionRegistry = Map<string, Action<any>>;
|
||||
export type ActionRegistry = Map<string, ActionByType<any>>;
|
||||
export type TriggerToActionsRegistry = Map<TriggerId, string[]>;
|
||||
|
||||
const DEFAULT_TRIGGER = '';
|
||||
|
||||
export type TriggerId = keyof TriggerContextMapping;
|
||||
|
||||
export type BaseContext = object;
|
||||
export type TriggerContext = BaseContext;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export default function({ getService }: PluginFunctionalProviderContext) {
|
|||
|
||||
it('Can create a new child', async () => {
|
||||
await testSubjects.click('embeddablePanelToggleMenuIcon');
|
||||
await testSubjects.click('embeddablePanelAction-ADD_PANEL_ACTION_ID');
|
||||
await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL');
|
||||
await testSubjects.click('createNew');
|
||||
await testSubjects.click('createNew-TODO_EMBEDDABLE');
|
||||
await testSubjects.setValue('taskInputField', 'new task');
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function({ getService }: PluginFunctionalProviderContext) {
|
|||
await testSubjects.click('addDynamicAction');
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('emitHelloWorldTrigger');
|
||||
await testSubjects.click('embeddablePanelAction-HELLO_WORLD_ACTION_TYPE-Waldo');
|
||||
await testSubjects.click('embeddablePanelAction-ACTION_HELLO_WORLD-Waldo');
|
||||
});
|
||||
await retry.try(async () => {
|
||||
const text = await testSubjects.getVisibleText('dynamicHelloWorldActionText');
|
||||
|
|
|
@ -21,7 +21,7 @@ const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel';
|
|||
const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel';
|
||||
const REPLACE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-replacePanel';
|
||||
const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel';
|
||||
const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-CUSTOMIZE_PANEL_ACTION_ID';
|
||||
const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL';
|
||||
const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon';
|
||||
const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -21,18 +21,22 @@ 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 {
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
export const SAMPLE_PANEL_ACTION = 'SAMPLE_PANEL_ACTION' as ActionType;
|
||||
|
||||
export interface SamplePanelActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
function createSamplePanelAction() {
|
||||
return createAction<ActionContext>({
|
||||
type: 'samplePanelAction',
|
||||
return createAction<typeof SAMPLE_PANEL_ACTION>({
|
||||
type: SAMPLE_PANEL_ACTION,
|
||||
getDisplayName: () => 'Sample Panel Action',
|
||||
execute: async ({ embeddable }) => {
|
||||
execute: async ({ embeddable }: SamplePanelActionContext) => {
|
||||
if (!embeddable) {
|
||||
return;
|
||||
}
|
||||
|
@ -59,4 +63,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);
|
||||
|
|
|
@ -17,12 +17,16 @@
|
|||
* 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';
|
||||
|
||||
// Casting to ActionType is a hack - in a real situation use
|
||||
// declare module and add this id to ActionContextMapping.
|
||||
export const SAMPLE_PANEL_LINK = 'samplePanelLink' as ActionType;
|
||||
|
||||
export const createSamplePanelLink = (): Action =>
|
||||
createAction({
|
||||
type: 'samplePanelLink',
|
||||
createAction<typeof SAMPLE_PANEL_LINK>({
|
||||
type: SAMPLE_PANEL_LINK,
|
||||
getDisplayName: () => 'Sample panel Link',
|
||||
execute: async () => {},
|
||||
getHref: () => 'https://example.com/kibana/test',
|
||||
|
@ -30,4 +34,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);
|
||||
|
|
|
@ -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<ActionContext> {
|
||||
declare module '../../../../../../src/plugins/ui_actions/public' {
|
||||
export interface ActionContextMapping {
|
||||
[CSV_REPORTING_ACTION]: CSVActionContext;
|
||||
}
|
||||
}
|
||||
|
||||
class GetCsvReportPanelAction implements ActionByType<typeof CSV_REPORTING_ACTION> {
|
||||
private isDownloading: boolean;
|
||||
public readonly type = CSV_REPORTING_ACTION;
|
||||
public readonly id = CSV_REPORTING_ACTION;
|
||||
|
@ -64,13 +73,13 @@ class GetCsvReportPanelAction implements Action<ActionContext> {
|
|||
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<ActionContext> {
|
|||
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);
|
||||
|
|
|
@ -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<TimeRangeInput>;
|
||||
}
|
||||
|
||||
export class CustomTimeRangeAction implements Action<ActionContext> {
|
||||
export class CustomTimeRangeAction implements ActionByType<typeof CUSTOM_TIME_RANGE> {
|
||||
public readonly type = CUSTOM_TIME_RANGE;
|
||||
private openModal: OpenModal;
|
||||
private dateFormat?: string;
|
||||
|
@ -70,7 +70,7 @@ export class CustomTimeRangeAction implements Action<ActionContext> {
|
|||
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<ActionContext> {
|
|||
);
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: ActionContext) {
|
||||
public async execute({ embeddable }: TimeRangeActionContext) {
|
||||
const isCompatible = await this.isCompatible({ embeddable });
|
||||
if (!isCompatible) {
|
||||
throw new IncompatibleActionError();
|
||||
|
|
|
@ -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<TimeRangeInput>).getInput().timeRange !== undefined;
|
||||
}
|
||||
|
||||
interface ActionContext {
|
||||
export interface TimeBadgeActionContext {
|
||||
embeddable: Embeddable<TimeRangeInput>;
|
||||
}
|
||||
|
||||
export class CustomTimeRangeBadge implements Action<ActionContext> {
|
||||
export class CustomTimeRangeBadge implements ActionByType<typeof CUSTOM_TIME_RANGE_BADGE> {
|
||||
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<ActionContext> {
|
|||
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<ActionContext> {
|
|||
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();
|
||||
|
|
|
@ -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<Setup, Start, SetupDependencies, StartDependencies> {
|
||||
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() {}
|
||||
|
|
|
@ -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<CoreStart['overlays']>;
|
||||
}
|
||||
|
||||
export class FlyoutCreateDrilldownAction implements Action<FlyoutCreateDrilldownActionContext> {
|
||||
export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLYOUT_ADD_DRILLDOWN> {
|
||||
public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN;
|
||||
public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN;
|
||||
public order = 5;
|
||||
|
|
|
@ -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<DrilldownService, 'registerDrilldown'
|
|||
// eslint-disable-next-line
|
||||
export interface DrilldownsStartContract {}
|
||||
|
||||
declare module '../../../../src/plugins/ui_actions/public' {
|
||||
export interface ActionContextMapping {
|
||||
[OPEN_FLYOUT_ADD_DRILLDOWN]: FlyoutCreateDrilldownActionContext;
|
||||
}
|
||||
}
|
||||
|
||||
export class DrilldownsPlugin
|
||||
implements
|
||||
Plugin<
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup } from 'src/core/public';
|
||||
import { CONTEXT_MENU_TRIGGER } from '../../../../../src/plugins/embeddable/public';
|
||||
import { FlyoutCreateDrilldownAction } from '../actions';
|
||||
import { DrilldownsSetupDependencies } from '../plugin';
|
||||
|
||||
|
@ -15,7 +16,7 @@ export class DrilldownService {
|
|||
});
|
||||
|
||||
uiActions.registerAction(actionFlyoutCreateDrilldown);
|
||||
uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionFlyoutCreateDrilldown.id);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue