Move actions to NP (#44707)

* move actions to np

* fix jest and types
This commit is contained in:
Stacey Gammon 2019-09-18 21:50:34 -04:00 committed by GitHub
parent 1ef6373f77
commit 01daabcb53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
124 changed files with 1422 additions and 1029 deletions

View file

@ -28,7 +28,8 @@
"kbnESQuery": "packages/kbn-es-query",
"inspector": "src/plugins/inspector",
"kibana-react": "src/plugins/kibana_react",
"esUi": "src/plugins/es_ui_shared"
"esUi": "src/plugins/es_ui_shared",
"uiActions": "src/plugins/ui_actions"
},
"exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"],
"translations": []

View file

@ -51,6 +51,7 @@ export default {
'!src/legacy/ui/public/{agg_types,vis}/**/*.d.ts',
],
moduleNameMapper: {
'^src/plugins/(.*)': '<rootDir>/src/plugins/$1',
'^plugins/([^\/.]*)(.*)': '<rootDir>/src/legacy/core_plugins/$1/public$2',
'^ui/(.*)': '<rootDir>/src/legacy/ui/public/$1',
'^uiExports/(.*)': '<rootDir>/src/dev/jest/mocks/file_mock.js',

View file

@ -3,7 +3,8 @@
"version": "kibana",
"requiredPlugins": [
"embeddable",
"inspector"
"inspector",
"ui_actions"
],
"server": false,
"ui": true

View file

@ -33,6 +33,7 @@ const pluginInstance = plugin({} as any);
export const setup = pluginInstance.setup(npSetup.core, {
embeddable: embeddableSetup,
uiActions: npSetup.plugins.uiActions,
});
export const start = pluginInstance.start(npStart.core, {
@ -42,4 +43,5 @@ export const start = pluginInstance.start(npStart.core, {
SavedObjectFinder,
ExitFullScreenButton,
},
uiActions: npStart.plugins.uiActions,
});

View file

@ -53,6 +53,7 @@ beforeEach(async () => {
notifications: {} as any,
overlays: {} as any,
savedObjectMetaData: {} as any,
uiActions: {} as any,
};
const input = getSampleDashboardInput({
panels: {

View file

@ -18,12 +18,12 @@
*/
import { i18n } from '@kbn/i18n';
import {
Action,
IEmbeddable,
IncompatibleActionError,
} from '../../../../../../embeddable_api/public/np_ready/public';
import { IEmbeddable } from '../../../../../../embeddable_api/public/np_ready/public';
import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable';
import {
IAction,
IncompatibleActionError,
} from '../../../../../../../../../src/plugins/ui_actions/public';
export const EXPAND_PANEL_ACTION = 'togglePanel';
@ -43,13 +43,12 @@ interface ActionContext {
embeddable: IEmbeddable;
}
export class ExpandPanelAction extends Action<ActionContext> {
export class ExpandPanelAction implements IAction<ActionContext> {
public readonly type = EXPAND_PANEL_ACTION;
public readonly id = EXPAND_PANEL_ACTION;
public order = 7;
constructor() {
super(EXPAND_PANEL_ACTION);
this.order = 7;
}
constructor() {}
public getDisplayName({ embeddable }: ActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {

View file

@ -45,6 +45,7 @@ const options: DashboardContainerOptions = {
inspector: {} as any,
SavedObjectFinder: () => null,
ExitFullScreenButton: () => null,
uiActions: {} as any,
};
beforeEach(() => {

View file

@ -22,6 +22,7 @@ import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { Filter } from '@kbn/es-query';
import { RefreshInterval, TimeRange } from 'src/plugins/data/public';
import { IUiActionsStart } from '../../../../../../../../plugins/ui_actions/public';
import {
Container,
ContainerInput,
@ -82,6 +83,7 @@ export interface DashboardContainerOptions {
inspector: InspectorStartContract;
SavedObjectFinder: React.ComponentType<any>;
ExitFullScreenButton: React.ComponentType<any>;
uiActions: IUiActionsStart;
}
export type DashboardReactContextValue = KibanaReactContextValue<DashboardContainerOptions>;

View file

@ -68,6 +68,7 @@ function prepare(props?: Partial<DashboardGridProps>) {
inspector: {} as any,
SavedObjectFinder: () => null,
ExitFullScreenButton: () => null,
uiActions: {} as any,
};
dashboardContainer = new DashboardContainer(initialInput, options);
const defaultTestProps: DashboardGridProps = {

View file

@ -266,7 +266,7 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
<EmbeddableChildPanel
embeddableId={panel.explicitInput.id}
container={this.props.container}
getActions={this.props.kibana.services.embeddable.getTriggerCompatibleActions}
getActions={this.props.kibana.services.uiActions.getTriggerCompatibleActions}
getEmbeddableFactory={this.props.kibana.services.embeddable.getEmbeddableFactory}
getAllEmbeddableFactories={this.props.kibana.services.embeddable.getEmbeddableFactories}
overlays={this.props.kibana.services.overlays}

View file

@ -59,6 +59,7 @@ function getProps(
inspector: {} as any,
SavedObjectFinder: () => null,
ExitFullScreenButton,
uiActions: {} as any,
};
const input = getSampleDashboardInput({

View file

@ -18,17 +18,20 @@
*/
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { IUiActionsSetup, IUiActionsStart } from '../../../../../../plugins/ui_actions/public';
import { CONTEXT_MENU_TRIGGER, Plugin as EmbeddablePlugin } from './lib/embeddable_api';
import { ExpandPanelAction, DashboardContainerFactory } from './lib';
import { Start as InspectorStartContract } from '../../../../../../plugins/inspector/public';
interface SetupDependencies {
embeddable: ReturnType<EmbeddablePlugin['setup']>;
uiActions: IUiActionsSetup;
}
interface StartDependencies {
embeddable: ReturnType<EmbeddablePlugin['start']>;
inspector: InspectorStartContract;
uiActions: IUiActionsStart;
__LEGACY: {
SavedObjectFinder: React.ComponentType<any>;
ExitFullScreenButton: React.ComponentType<any>;
@ -42,15 +45,15 @@ export class DashboardEmbeddableContainerPublicPlugin
implements Plugin<Setup, Start, SetupDependencies, StartDependencies> {
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { embeddable }: SetupDependencies): Setup {
public setup(core: CoreSetup, { embeddable, uiActions }: SetupDependencies): Setup {
const expandPanelAction = new ExpandPanelAction();
embeddable.registerAction(expandPanelAction);
embeddable.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id);
uiActions.registerAction(expandPanelAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id);
}
public start(core: CoreStart, plugins: StartDependencies): Start {
const { application, notifications, overlays } = core;
const { embeddable, inspector, __LEGACY } = plugins;
const { embeddable, inspector, __LEGACY, uiActions } = plugins;
const factory = new DashboardContainerFactory({
application,
@ -60,6 +63,7 @@ export class DashboardEmbeddableContainerPublicPlugin
inspector,
SavedObjectFinder: __LEGACY.SavedObjectFinder,
ExitFullScreenButton: __LEGACY.ExitFullScreenButton,
uiActions,
});
embeddable.registerEmbeddableFactory(factory.type, factory);

View file

@ -39,18 +39,21 @@ import {
ContactCardEmbeddableOutput,
} from '../../../../../embeddable_api/public/np_ready/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable';
import { embeddablePluginMock } from '../../../../../embeddable_api/public/np_ready/public/mocks';
import { EditModeAction } from '../../../../../embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action';
import { createEditModeAction } from '../../../../../embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action';
// eslint-disable-next-line
import { inspectorPluginMock } from '../../../../../../../plugins/inspector/public/mocks';
import { KibanaContextProvider } from '../../../../../../../plugins/kibana_react/public';
// eslint-disable-next-line
import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks';
test('DashboardContainer in edit mode shows edit mode actions', async () => {
const inspector = inspectorPluginMock.createStartContract();
const { setup, doStart } = embeddablePluginMock.createInstance();
const uiActionsSetup = uiActionsPluginMock.createSetupContract();
const editModeAction = new EditModeAction();
setup.registerAction(editModeAction);
setup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction.id);
const editModeAction = createEditModeAction();
uiActionsSetup.registerAction(editModeAction);
uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction.id);
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
@ -67,6 +70,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
inspector: {} as any,
SavedObjectFinder: () => null,
ExitFullScreenButton: () => null,
uiActions: {} as any,
};
const container = new DashboardContainer(initialInput, options);

View file

@ -2,5 +2,9 @@
"id": "embeddable",
"version": "kibana",
"server": false,
"ui": true
"ui": true,
"requiredPlugins": [
"embeddable",
"inspector"
]
}

View file

@ -23,32 +23,16 @@ import {
EmbeddableApi,
EmbeddableDependenciesInternal,
} from './types';
import { attachAction } from './attach_action';
import { detachAction } from './detach_action';
import { executeTriggerActions } from './execute_trigger_actions';
import { getEmbeddableFactories } from './get_embeddable_factories';
import { getEmbeddableFactory } from './get_embeddable_factory';
import { getTrigger } from './get_trigger';
import { getTriggerActions } from './get_trigger_actions';
import { getTriggerCompatibleActions } from './get_trigger_compatible_actions';
import { registerAction } from './register_action';
import { registerEmbeddableFactory } from './register_embeddable_factory';
import { registerTrigger } from './register_trigger';
export * from './types';
export const pureApi: EmbeddableApiPure = {
attachAction,
detachAction,
executeTriggerActions,
getEmbeddableFactories,
getEmbeddableFactory,
getTrigger,
getTriggerActions,
getTriggerCompatibleActions,
registerAction,
registerEmbeddableFactory,
registerTrigger,
};
export const createApi = (deps: EmbeddableDependencies) => {

View file

@ -21,8 +21,6 @@ import { EmbeddableDependencies } from '../types';
export const createDeps = (): EmbeddableDependencies => {
const deps: EmbeddableDependencies = {
triggers: new Map<any, any>(),
actions: new Map<any, any>(),
embeddableFactories: new Map<any, any>(),
};
return deps;

View file

@ -19,151 +19,6 @@
import { createApi } from '..';
import { createDeps } from './helpers';
import { expectError } from '../../tests/helpers';
const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
test('can register trigger', () => {
const deps = createDeps();
const { api } = createApi(deps);
api.registerTrigger({
actionIds: [],
description: 'foo',
id: 'bar',
title: 'baz',
});
expect(deps.triggers.get('bar')).toEqual({
actionIds: [],
description: 'foo',
id: 'bar',
title: 'baz',
});
});
test('can register action', () => {
const deps = createDeps();
const { api } = createApi(deps);
api.registerAction({
id: HELLO_WORLD_ACTION_ID,
order: 13,
} as any);
expect(deps.actions.get(HELLO_WORLD_ACTION_ID)).toMatchObject({
id: HELLO_WORLD_ACTION_ID,
order: 13,
});
});
test('can attach an action to a trigger', () => {
const deps = createDeps();
const { api } = createApi(deps);
const trigger = {
id: 'MY-TRIGGER',
actionIds: [],
};
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
expect(trigger.actionIds).toEqual([]);
api.registerTrigger(trigger);
api.registerAction(action);
api.attachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
expect(trigger.actionIds).toEqual([HELLO_WORLD_ACTION_ID]);
});
test('can detach an action to a trigger', () => {
const deps = createDeps();
const { api } = createApi(deps);
const trigger = {
id: 'MY-TRIGGER',
actionIds: [],
};
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
expect(trigger.actionIds).toEqual([]);
api.registerTrigger(trigger);
api.registerAction(action);
api.attachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
api.detachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
expect(trigger.actionIds).toEqual([]);
});
test('detaching an invalid action from a trigger throws an error', async () => {
const deps = createDeps();
const { api } = createApi(deps);
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
api.registerAction(action);
const error = expectError(() => api.detachAction('i do not exist', HELLO_WORLD_ACTION_ID));
expect(error).toBeInstanceOf(Error);
expect(error.message).toMatchInlineSnapshot(
`"No trigger [triggerId = i do not exist] exists, for detaching action [actionId = HELLO_WORLD_ACTION_ID]."`
);
});
test('attaching an invalid action to a trigger throws an error', async () => {
const deps = createDeps();
const { api } = createApi(deps);
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
api.registerAction(action);
const error = expectError(() => api.attachAction('i do not exist', HELLO_WORLD_ACTION_ID));
expect(error).toBeInstanceOf(Error);
expect(error.message).toMatchInlineSnapshot(
`"No trigger [triggerId = i do not exist] exists, for attaching action [actionId = HELLO_WORLD_ACTION_ID]."`
);
});
test('cannot register another action with the same ID', async () => {
const deps = createDeps();
const { api } = createApi(deps);
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
api.registerAction(action);
const error = expectError(() => api.registerAction(action));
expect(error).toBeInstanceOf(Error);
expect(error.message).toMatchInlineSnapshot(
`"Action [action.id = HELLO_WORLD_ACTION_ID] already registered in Embeddables API."`
);
});
test('cannot register another trigger with the same ID', async () => {
const deps = createDeps();
const { api } = createApi(deps);
const trigger = { id: 'MY-TRIGGER' } as any;
api.registerTrigger(trigger);
const error = expectError(() => api.registerTrigger(trigger));
expect(error).toBeInstanceOf(Error);
expect(error.message).toMatchInlineSnapshot(
`"Trigger [trigger.id = MY-TRIGGER] already registered in Embeddables API."`
);
});
test('cannot register embeddable factory with the same ID', async () => {
const deps = createDeps();
@ -172,12 +27,7 @@ test('cannot register embeddable factory with the same ID', async () => {
const embeddableFactory = {} as any;
api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory);
const error = expectError(() =>
api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory)
);
expect(error).toBeInstanceOf(Error);
expect(error.message).toMatchInlineSnapshot(
`"Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API."`
expect(() => api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory)).toThrowError(
'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.'
);
});

View file

@ -17,34 +17,18 @@
* under the License.
*/
import { TriggerRegistry, ActionRegistry, EmbeddableFactoryRegistry } from '../types';
import {
Trigger,
Action,
EmbeddableFactory,
ExecuteTriggerActions,
GetEmbeddableFactories,
} from '../lib';
import { EmbeddableFactoryRegistry } from '../types';
import { EmbeddableFactory, GetEmbeddableFactories } from '../lib';
export interface EmbeddableApi {
attachAction: (triggerId: string, actionId: string) => void;
detachAction: (triggerId: string, actionId: string) => void;
executeTriggerActions: ExecuteTriggerActions;
getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory;
getEmbeddableFactories: GetEmbeddableFactories;
getTrigger: (id: string) => Trigger;
getTriggerActions: (id: string) => Action[];
getTriggerCompatibleActions: <C>(triggerId: string, context: C) => Promise<Array<Action<C>>>;
registerAction: (action: Action) => void;
// TODO: Make `registerEmbeddableFactory` receive only `factory` argument.
registerEmbeddableFactory: (id: string, factory: EmbeddableFactory) => void;
registerTrigger: (trigger: Trigger) => void;
}
export interface EmbeddableDependencies {
actions: ActionRegistry;
embeddableFactories: EmbeddableFactoryRegistry;
triggers: TriggerRegistry;
}
export interface EmbeddableDependenciesInternal extends EmbeddableDependencies {

View file

@ -17,11 +17,11 @@
* under the License.
*/
import { EmbeddableApi } from './api/types';
import { IUiActionsSetup } from 'src/plugins/ui_actions/public';
import {
CONTEXT_MENU_TRIGGER,
APPLY_FILTER_TRIGGER,
ApplyFilterAction,
createFilterAction,
PANEL_BADGE_TRIGGER,
} from './lib';
@ -31,7 +31,7 @@ import {
*
* @param api
*/
export const bootstrap = (api: EmbeddableApi) => {
export const bootstrap = (uiActions: IUiActionsSetup) => {
const triggerContext = {
id: CONTEXT_MENU_TRIGGER,
title: 'Context menu',
@ -50,11 +50,11 @@ export const bootstrap = (api: EmbeddableApi) => {
description: 'Actions appear in title bar when an embeddable loads in a panel',
actionIds: [],
};
const actionApplyFilter = new ApplyFilterAction();
const actionApplyFilter = createFilterAction();
api.registerTrigger(triggerContext);
api.registerTrigger(triggerFilter);
api.registerAction(actionApplyFilter);
api.registerTrigger(triggerBadge);
api.attachAction(triggerFilter.id, actionApplyFilter.id);
uiActions.registerTrigger(triggerContext);
uiActions.registerTrigger(triggerFilter);
uiActions.registerAction(actionApplyFilter);
uiActions.registerTrigger(triggerBadge);
uiActions.attachAction(triggerFilter.id, actionApplyFilter.id);
};

View file

@ -25,10 +25,8 @@ export {
APPLY_FILTER_ACTION,
APPLY_FILTER_TRIGGER,
PANEL_BADGE_TRIGGER,
Action,
Adapters,
AddPanelAction,
ApplyFilterAction,
CONTEXT_MENU_TRIGGER,
Container,
ContainerInput,
@ -45,19 +43,15 @@ export {
EmbeddableOutput,
EmbeddablePanel,
ErrorEmbeddable,
ExecuteTriggerActions,
GetActionsCompatibleWithTrigger,
GetEmbeddableFactories,
GetEmbeddableFactory,
IContainer,
IEmbeddable,
IncompatibleActionError,
OutputSpec,
PanelNotFoundError,
PanelState,
PropertySpec,
SavedObjectMetaData,
Trigger,
ViewMode,
isErrorEmbeddable,
openAddPanelFlyout,

View file

@ -24,5 +24,5 @@ import { npSetup, npStart } from 'ui/new_platform';
import { plugin } from '.';
const pluginInstance = plugin({} as any);
export const setup = pluginInstance.setup(npSetup.core);
export const setup = pluginInstance.setup(npSetup.core, { uiActions: npSetup.plugins.uiActions });
export const start = pluginInstance.start(npStart.core);

View file

@ -17,29 +17,23 @@
* under the License.
*/
import { Action } from './action';
import { ApplyFilterAction } from './apply_filter_action';
import { createFilterAction } from './apply_filter_action';
import { expectError } from '../../tests/helpers';
test('is instance of Action', () => {
const action = new ApplyFilterAction();
expect(action).toBeInstanceOf(Action);
});
test('has APPLY_FILTER_ACTION type and id', () => {
const action = new ApplyFilterAction();
const action = createFilterAction();
expect(action.id).toBe('APPLY_FILTER_ACTION');
expect(action.type).toBe('APPLY_FILTER_ACTION');
});
test('has expected display name', () => {
const action = new ApplyFilterAction();
expect(action.getDisplayName()).toMatchInlineSnapshot(`"Apply filter to current view"`);
const action = createFilterAction();
expect(action.getDisplayName({} as any)).toMatchInlineSnapshot(`"Apply filter to current view"`);
});
describe('isCompatible()', () => {
test('when embeddable filters and triggerContext filters exist, returns true', async () => {
const action = new ApplyFilterAction();
test('when embeddable filters and filters exist, returns true', async () => {
const action = createFilterAction();
const result = await action.isCompatible({
embeddable: {
getRoot: () => ({
@ -54,7 +48,7 @@ describe('isCompatible()', () => {
});
test('when embeddable filters not set, returns false', async () => {
const action = new ApplyFilterAction();
const action = createFilterAction();
const result = await action.isCompatible({
embeddable: {
getRoot: () => ({
@ -68,8 +62,8 @@ describe('isCompatible()', () => {
expect(result).toBe(false);
});
test('when triggerContext or triggerContext filters are not set, returns false', async () => {
const action = new ApplyFilterAction();
test('when triggerContext or filters are not set, returns false', async () => {
const action = createFilterAction();
const result1 = await action.isCompatible({
embeddable: {
@ -98,9 +92,9 @@ const getEmbeddable = () => {
};
describe('execute()', () => {
describe('when triggerContext not set', () => {
describe('when no filters are given', () => {
test('throws an error', async () => {
const action = new ApplyFilterAction();
const action = createFilterAction();
const error = expectError(() =>
action.execute({
embeddable: getEmbeddable(),
@ -109,8 +103,8 @@ describe('execute()', () => {
expect(error).toBeInstanceOf(Error);
});
test('updates filter input on success', async done => {
const action = new ApplyFilterAction();
test('updates filter input on success', async () => {
const action = createFilterAction();
const [embeddable, root] = getEmbeddable();
await action.execute({
@ -122,23 +116,6 @@ describe('execute()', () => {
expect(root.updateInput.mock.calls[0][0]).toMatchObject({
filters: ['FILTER'],
});
done();
});
test('checks if action isCompatible', async done => {
const action = new ApplyFilterAction();
const spy = jest.spyOn(action, 'isCompatible');
const [embeddable] = getEmbeddable();
await action.execute({
embeddable,
filters: ['FILTER' as any],
});
expect(spy).toHaveBeenCalledTimes(1);
done();
});
});
});

View file

@ -19,9 +19,12 @@
import { i18n } from '@kbn/i18n';
import { Filter } from '@kbn/es-query';
import {
IAction,
createAction,
IncompatibleActionError,
} from '../../../../../../../../plugins/ui_actions/public';
import { IEmbeddable, EmbeddableInput } from '../embeddables';
import { Action } from './action';
import { IncompatibleActionError } from '../errors';
export const APPLY_FILTER_ACTION = 'APPLY_FILTER_ACTION';
@ -31,40 +34,38 @@ interface ActionContext {
filters: Filter[];
}
export class ApplyFilterAction extends Action<ActionContext> {
public readonly type = APPLY_FILTER_ACTION;
constructor() {
super(APPLY_FILTER_ACTION);
}
public getDisplayName() {
return i18n.translate('embeddableApi.actions.applyFilterActionTitle', {
defaultMessage: 'Apply filter to current view',
});
}
public async isCompatible(context: ActionContext) {
if (context.embeddable === undefined) {
return false;
}
const root = context.embeddable.getRoot() as RootEmbeddable;
return Boolean(root.getInput().filters !== undefined && context.filters !== undefined);
}
public async execute({ embeddable, filters }: ActionContext) {
if (!filters || !embeddable) {
throw new Error('Applying a filter requires a filter and embeddable as context');
}
if (!(await this.isCompatible({ embeddable, filters }))) {
throw new IncompatibleActionError();
}
const root = embeddable.getRoot() as RootEmbeddable;
root.updateInput({
filters,
});
async function isCompatible(context: ActionContext) {
if (context.embeddable === undefined) {
return false;
}
const root = context.embeddable.getRoot() as RootEmbeddable;
return Boolean(root.getInput().filters !== undefined && context.filters !== undefined);
}
export function createFilterAction(): IAction<ActionContext> {
return createAction<ActionContext>({
type: APPLY_FILTER_ACTION,
id: APPLY_FILTER_ACTION,
getDisplayName: () => {
return i18n.translate('embeddableApi.actions.applyFilterActionTitle', {
defaultMessage: 'Apply filter to current view',
});
},
isCompatible,
execute: async ({ embeddable, filters }) => {
if (!filters || !embeddable) {
throw new Error('Applying a filter requires a filter and embeddable as context');
}
if (!(await isCompatible({ embeddable, filters }))) {
throw new IncompatibleActionError();
}
const root = embeddable.getRoot() as RootEmbeddable;
root.updateInput({
filters,
});
},
});
}

View file

@ -18,7 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { Action } from './action';
import { IAction } from 'src/plugins/ui_actions/public';
import { GetEmbeddableFactory, ViewMode } from '../types';
import { EmbeddableFactoryNotFoundError } from '../errors';
import { IEmbeddable } from '../embeddables';
@ -29,12 +29,12 @@ interface ActionContext {
embeddable: IEmbeddable;
}
export class EditPanelAction extends Action<ActionContext> {
export class EditPanelAction implements IAction<ActionContext> {
public readonly type = EDIT_PANEL_ACTION_ID;
constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) {
super(EDIT_PANEL_ACTION_ID);
this.order = 15;
}
public readonly id = EDIT_PANEL_ACTION_ID;
public order = 15;
constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) {}
public getDisplayName({ embeddable }: ActionContext) {
const factory = this.getEmbeddableFactory(embeddable.type);

View file

@ -17,6 +17,5 @@
* under the License.
*/
export { Action } from './action';
export * from './apply_filter_action';
export * from './edit_panel_action';

View file

@ -23,21 +23,19 @@ import React from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import { Subscription } from 'rxjs';
import { CoreStart } from 'src/core/public';
import { TGetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public';
import { ErrorEmbeddable, IEmbeddable } from '../embeddables';
import { EmbeddablePanel } from '../panel';
import { IContainer } from './i_container';
import {
GetActionsCompatibleWithTrigger,
GetEmbeddableFactory,
GetEmbeddableFactories,
} from '../types';
import { GetEmbeddableFactory, GetEmbeddableFactories } from '../types';
import { Start as InspectorStartContract } from '../../../../../../../../plugins/inspector/public';
export interface EmbeddableChildPanelProps {
embeddableId: string;
className?: string;
container: IContainer;
getActions: GetActionsCompatibleWithTrigger;
getActions: TGetActionsCompatibleWithTrigger;
getEmbeddableFactory: GetEmbeddableFactory;
getAllEmbeddableFactories: GetEmbeddableFactories;
overlays: CoreStart['overlays'];

View file

@ -17,11 +17,8 @@
* under the License.
*/
import {
IncompatibleActionError,
PanelNotFoundError,
EmbeddableFactoryNotFoundError,
} from './errors';
import { IncompatibleActionError } from 'src/plugins/ui_actions/public';
import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from './errors';
describe('IncompatibleActionError', () => {
test('is instance of error', () => {

View file

@ -20,18 +20,6 @@
/* eslint-disable max-classes-per-file */
import { i18n } from '@kbn/i18n';
export class IncompatibleActionError extends Error {
code = 'INCOMPATIBLE_ACTION';
constructor() {
super(
i18n.translate('embeddableApi.errors.incompatibleAction', {
defaultMessage: 'Action is incompatible',
})
);
}
}
export class PanelNotFoundError extends Error {
code = 'PANEL_NOT_FOUND';

View file

@ -24,4 +24,3 @@ export * from './actions';
export * from './triggers';
export * from './containers';
export * from './panel';
export * from './context_menu_actions';

View file

@ -25,11 +25,11 @@ 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 } from '../actions';
import { IAction, ITrigger } from 'src/plugins/ui_actions/public';
import { Trigger, GetEmbeddableFactory, ViewMode } from '../types';
import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables';
import { EmbeddablePanel } from './embeddable_panel';
import { EditModeAction } from '../test_samples/actions/edit_mode_action';
import { createEditModeAction } from '../test_samples/actions';
import {
ContactCardEmbeddableFactory,
CONTACT_CARD_EMBEDDABLE,
@ -43,12 +43,12 @@ import {
// eslint-disable-next-line
import { inspectorPluginMock } from '../../../../../../../../plugins/inspector/public/mocks';
const actionRegistry = new Map<string, Action>();
const triggerRegistry = new Map<string, Trigger>();
const actionRegistry = new Map<string, IAction>();
const triggerRegistry = new Map<string, ITrigger>();
const embeddableFactories = new Map<string, EmbeddableFactory>();
const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id);
const editModeAction = new EditModeAction();
const editModeAction = createEditModeAction();
const trigger: Trigger = {
id: CONTEXT_MENU_TRIGGER,
actionIds: [editModeAction.id],

View file

@ -20,30 +20,29 @@ import { EuiContextMenuPanelDescriptor, EuiPanel } from '@elastic/eui';
import classNames from 'classnames';
import React from 'react';
import { Subscription } from 'rxjs';
import {
buildContextMenuForActions,
TGetActionsCompatibleWithTrigger,
IAction,
} from '../../../../../../../../plugins/ui_actions/public';
import { CoreStart } from '../../../../../../../../core/public';
import { buildContextMenuForActions } from '../context_menu_actions';
import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER } from '../triggers';
import { IEmbeddable } from '../embeddables/i_embeddable';
import {
ViewMode,
GetActionsCompatibleWithTrigger,
GetEmbeddableFactory,
GetEmbeddableFactories,
} from '../types';
import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../types';
import { RemovePanelAction } from './panel_header/panel_actions';
import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel_action';
import { CustomizePanelTitleAction } from './panel_header/panel_actions/customize_title/customize_panel_action';
import { PanelHeader } from './panel_header/panel_header';
import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action';
import { EditPanelAction, Action } from '../actions';
import { EditPanelAction } from '../actions';
import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal';
import { Start as InspectorStartContract } from '../../../../../../../../plugins/inspector/public';
interface Props {
embeddable: IEmbeddable<any, any>;
getActions: GetActionsCompatibleWithTrigger;
getActions: TGetActionsCompatibleWithTrigger;
getEmbeddableFactory: GetEmbeddableFactory;
getAllEmbeddableFactories: GetEmbeddableFactories;
overlays: CoreStart['overlays'];
@ -58,7 +57,7 @@ interface State {
viewMode: ViewMode;
hidePanelTitles: boolean;
closeContextMenu: boolean;
badges: Action[];
badges: IAction[];
}
export class EmbeddablePanel extends React.Component<Props, State> {
@ -211,7 +210,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
// These actions are exposed on the context menu for every embeddable, they bypass the trigger
// registry.
const extraActions = [
const extraActions: Array<IAction<{ embeddable: IEmbeddable }>> = [
new CustomizePanelTitleAction(createGetUserData(this.props.overlays)),
new AddPanelAction(
this.props.getEmbeddableFactory,
@ -225,8 +224,10 @@ export class EmbeddablePanel extends React.Component<Props, State> {
new EditPanelAction(this.props.getEmbeddableFactory),
];
const sorted = actions.concat(extraActions).sort((a: Action, b: Action) => {
return b.order - a.order;
const sorted = actions.concat(extraActions).sort((a: IAction, b: IAction) => {
const bOrder = b.order || 0;
const aOrder = a.order || 0;
return bOrder - aOrder;
});
return await buildContextMenuForActions({

View file

@ -30,6 +30,7 @@ import { FilterableEmbeddableFactory } from '../../../../test_samples/embeddable
import { FilterableContainer } from '../../../../test_samples/embeddables/filterable_container';
import { GetEmbeddableFactory } from '../../../../types';
import { coreMock } from '../../../../../../../../../../../core/public/mocks';
import { ContactCardEmbeddable } from '../../../../test_samples';
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
@ -88,10 +89,7 @@ test('Is not compatible when container is in view mode', async () => {
});
test('Is not compatible when embeddable is not a container', async () => {
expect(
// @ts-ignore
await action.isCompatible({ embeddable })
).toBe(false);
expect(await action.isCompatible({ embeddable } as any)).toBe(false);
});
test('Is compatible when embeddable is a parent and in edit mode', async () => {
@ -102,13 +100,15 @@ test('Is compatible when embeddable is a parent and in edit mode', async () => {
test('Execute throws an error when called with an embeddable that is not a container', async () => {
async function check() {
await action.execute({
// @ts-ignore
embeddable: new ContactCardEmbeddable({
firstName: 'sue',
id: '123',
viewMode: ViewMode.EDIT,
}),
});
embeddable: new ContactCardEmbeddable(
{
firstName: 'sue',
id: '123',
viewMode: ViewMode.EDIT,
},
{} as any
),
} as any);
}
await expect(check()).rejects.toThrow(Error);
});

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { IAction } from 'src/plugins/ui_actions/public';
import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types';
import { Action } from '../../../../actions';
import { openAddPanelFlyout } from './open_add_panel_flyout';
import { NotificationsStart } from '../../../../../../../../../../../core/public';
import { KibanaReactOverlays } from '../../../../../../../../../../../plugins/kibana_react/public';
@ -30,8 +30,9 @@ interface ActionContext {
embeddable: IContainer;
}
export class AddPanelAction extends Action<ActionContext> {
export class AddPanelAction implements IAction<ActionContext> {
public readonly type = ADD_PANEL_ACTION_ID;
public readonly id = ADD_PANEL_ACTION_ID;
constructor(
private readonly getFactory: GetEmbeddableFactory,
@ -39,9 +40,7 @@ export class AddPanelAction extends Action<ActionContext> {
private readonly overlays: KibanaReactOverlays,
private readonly notifications: NotificationsStart,
private readonly SavedObjectFinder: React.ComponentType<any>
) {
super(ADD_PANEL_ACTION_ID);
}
) {}
public getDisplayName() {
return i18n.translate('embeddableApi.addPanel.displayName', {

View file

@ -18,7 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { Action } from '../../../../actions';
import { IAction } from 'src/plugins/ui_actions/public';
import { ViewMode } from '../../../../types';
import { IEmbeddable } from '../../../../embeddables';
@ -30,11 +30,12 @@ interface ActionContext {
embeddable: IEmbeddable;
}
export class CustomizePanelTitleAction extends Action<ActionContext> {
export class CustomizePanelTitleAction implements IAction<ActionContext> {
public readonly type = CUSTOMIZE_PANEL_ACTION_ID;
public id = CUSTOMIZE_PANEL_ACTION_ID;
public order = 10;
constructor(private readonly getDataFromUser: GetUserData) {
super(CUSTOMIZE_PANEL_ACTION_ID);
this.order = 10;
}

View file

@ -18,7 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { Action } from '../../../actions';
import { IAction } from '../../../../../../../../../../plugins/ui_actions/public';
import { Start as InspectorStartContract } from '../../../../../../../../../../plugins/inspector/public';
import { IEmbeddable } from '../../../embeddables';
@ -28,13 +28,12 @@ interface ActionContext {
embeddable: IEmbeddable;
}
export class InspectPanelAction extends Action<ActionContext> {
export class InspectPanelAction implements IAction<ActionContext> {
public readonly type = INSPECT_PANEL_ACTION_ID;
public readonly id = INSPECT_PANEL_ACTION_ID;
public order = 10;
constructor(private readonly inspector: InspectorStartContract) {
super(INSPECT_PANEL_ACTION_ID);
this.order = 20;
}
constructor(private readonly inspector: InspectorStartContract) {}
public getDisplayName() {
return i18n.translate('embeddableApi.panel.inspectPanel.displayName', {

View file

@ -17,10 +17,12 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import {
IAction,
IncompatibleActionError,
} from '../../../../../../../../../../plugins/ui_actions/public';
import { ContainerInput, IContainer } from '../../../containers';
import { ViewMode } from '../../../types';
import { Action } from '../../../actions';
import { IncompatibleActionError } from '../../../errors';
import { IEmbeddable } from '../../../embeddables';
export const REMOVE_PANEL_ACTION = 'deletePanel';
@ -39,13 +41,12 @@ function hasExpandedPanelInput(
return (container as IContainer<{}, ExpandedPanelInput>).getInput().expandedPanelId !== undefined;
}
export class RemovePanelAction extends Action<ActionContext> {
export class RemovePanelAction implements IAction<ActionContext> {
public readonly type = REMOVE_PANEL_ACTION;
constructor() {
super(REMOVE_PANEL_ACTION);
public readonly id = REMOVE_PANEL_ACTION;
public order = 5;
this.order = 5;
}
constructor() {}
public getDisplayName() {
return i18n.translate('embeddableApi.panel.removePanel.displayName', {

View file

@ -20,8 +20,8 @@ import { i18n } from '@kbn/i18n';
import { EuiContextMenuPanelDescriptor, EuiBadge, EuiIcon, EuiToolTip } from '@elastic/eui';
import classNames from 'classnames';
import React from 'react';
import { IAction } from 'src/plugins/ui_actions/public';
import { PanelOptionsMenu } from './panel_options_menu';
import { Action } from '../../actions';
import { IEmbeddable } from '../../embeddables';
import { VisualizeEmbeddable } from '../../../../../../../kibana/public/visualize/embeddable/visualize_embeddable';
import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../../kibana/public/visualize/embeddable/constants';
@ -32,11 +32,11 @@ export interface PanelHeaderProps {
hidePanelTitles: boolean;
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor>;
closeContextMenu: boolean;
badges: Action[];
badges: IAction[];
embeddable: IEmbeddable;
}
function renderBadges(badges: Action[], embeddable: IEmbeddable) {
function renderBadges(badges: IAction[], embeddable: IEmbeddable) {
return badges.map(badge => (
<EuiBadge
key={badge.id}

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { createAction } from '../../../../../../../../../plugins/ui_actions/public';
import { ViewMode } from '../../types';
import { Action } from '../../actions';
import { IEmbeddable } from '../../embeddables';
export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION';
@ -26,22 +26,12 @@ export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION';
interface ActionContext {
embeddable: IEmbeddable;
}
export class EditModeAction extends Action<ActionContext> {
public readonly type = EDIT_MODE_ACTION;
constructor() {
super(EDIT_MODE_ACTION);
}
getDisplayName() {
return `I should only show up in edit mode`;
}
async isCompatible(context: ActionContext) {
return context.embeddable.getInput().viewMode === ViewMode.EDIT;
}
async execute() {
return;
}
export function createEditModeAction() {
return createAction<ActionContext>({
type: EDIT_MODE_ACTION,
getDisplayName: () => 'I only show up in edit mode',
isCompatible: async context => context.embeddable.getInput().viewMode === ViewMode.EDIT,
execute: async () => {},
});
}

View file

@ -18,8 +18,5 @@
*/
export * from './edit_mode_action';
export * from './get_message_modal';
export * from './hello_world_action';
export * from './restricted_action';
export * from './say_hello_action';
export * from './send_message_action';

View file

@ -17,9 +17,11 @@
* under the License.
*/
import { Action } from '../../actions';
import {
IAction,
IncompatibleActionError,
} from '../../../../../../../../../plugins/ui_actions/public';
import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables';
import { IncompatibleActionError } from '../../errors';
export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION';
@ -41,14 +43,15 @@ interface ActionContext {
message?: string;
}
export class SayHelloAction extends Action<ActionContext> {
export class SayHelloAction implements IAction<ActionContext> {
public readonly type = SAY_HELLO_ACTION;
public readonly id = SAY_HELLO_ACTION;
private sayHello: (name: string) => void;
// Taking in a function, instead of always directly interacting with the dom,
// can make testing the execute part of the action easier.
constructor(sayHello: (name: string) => void) {
super(SAY_HELLO_ACTION);
this.sayHello = sayHello;
}
@ -56,6 +59,10 @@ export class SayHelloAction extends Action<ActionContext> {
return 'Say hello';
}
getIconType() {
return undefined;
}
// Can use typescript generics to get compiler time warnings for immediate feedback if
// the context is not compatible.
async isCompatible(context: ActionContext) {

View file

@ -18,7 +18,10 @@
*/
import React from 'react';
import { EuiFlyoutBody } from '@elastic/eui';
import { Action, IncompatibleActionError } from '../..';
import {
createAction,
IncompatibleActionError,
} from '../../../../../../../../../plugins/ui_actions/public';
import { Embeddable, EmbeddableInput } from '../../embeddables';
import { GetMessageModal } from './get_message_modal';
import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action';
@ -30,41 +33,35 @@ interface ActionContext {
embeddable: Embeddable<EmbeddableInput, FullNameEmbeddableOutput>;
message: string;
}
export class SendMessageAction extends Action {
public readonly type = SEND_MESSAGE_ACTION;
constructor(private readonly overlays: CoreStart['overlays']) {
super(SEND_MESSAGE_ACTION);
}
const isCompatible = async (context: ActionContext) => hasFullNameOutput(context.embeddable);
getDisplayName() {
return 'Send message';
}
async isCompatible(context: ActionContext) {
return hasFullNameOutput(context.embeddable);
}
async sendMessage(context: ActionContext, message: string) {
export function createSendMessageAction(overlays: CoreStart['overlays']) {
const sendMessage = async (context: ActionContext, message: string) => {
const greeting = `Hello, ${context.embeddable.getOutput().fullName}`;
const content = message ? `${greeting}. ${message}` : greeting;
this.overlays.openFlyout(<EuiFlyoutBody>{content}</EuiFlyoutBody>);
}
overlays.openFlyout(<EuiFlyoutBody>{content}</EuiFlyoutBody>);
};
async execute(context: ActionContext) {
if (!(await this.isCompatible(context))) {
throw new IncompatibleActionError();
}
return createAction<ActionContext>({
type: SEND_MESSAGE_ACTION,
getDisplayName: () => 'Send message',
isCompatible,
execute: async context => {
if (!(await isCompatible(context))) {
throw new IncompatibleActionError();
}
const modal = this.overlays.openModal(
<GetMessageModal
onCancel={() => modal.close()}
onDone={message => {
modal.close();
this.sendMessage(context, message);
}}
/>
);
}
const modal = overlays.openModal(
<GetMessageModal
onCancel={() => modal.close()}
onDone={message => {
modal.close();
sendMessage(context, message);
}}
/>
);
},
});
}

View file

@ -28,12 +28,12 @@ import {
import { Subscription } from 'rxjs';
import { EuiButton } from '@elastic/eui';
import * as Rx from 'rxjs';
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import { ContactCardEmbeddable, CONTACT_USER_TRIGGER } from './contact_card_embeddable';
import { ExecuteTriggerActions } from '../../../types';
interface Props {
embeddable: ContactCardEmbeddable;
execTrigger: ExecuteTriggerActions;
execTrigger: TExecuteTriggerActions;
}
interface State {

View file

@ -19,11 +19,11 @@
import React from 'react';
import ReactDom from 'react-dom';
import { Subscription } from 'rxjs';
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import { Container } from '../../../containers';
import { EmbeddableOutput, Embeddable, EmbeddableInput } from '../../../embeddables';
import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory';
import { ContactCardEmbeddableComponent } from './contact_card';
import { ExecuteTriggerActions } from '../../../types';
export interface ContactCardEmbeddableInput extends EmbeddableInput {
firstName: string;
@ -37,7 +37,7 @@ export interface ContactCardEmbeddableOutput extends EmbeddableOutput {
}
export interface ContactCardEmbeddableOptions {
execAction: ExecuteTriggerActions;
execAction: TExecuteTriggerActions;
}
function getFullName(input: ContactCardEmbeddableInput) {

View file

@ -19,12 +19,13 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import { EmbeddableFactory } from '../../../embeddables';
import { Container } from '../../../containers';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
import { ContactCardInitializer } from './contact_card_initializer';
import { EmbeddableFactoryOptions } from '../../../embeddables/embeddable_factory';
import { ExecuteTriggerActions } from '../../../types';
import { CoreStart } from '../../../../../../../../../../core/public';
export const CONTACT_CARD_EMBEDDABLE = 'CONTACT_CARD_EMBEDDABLE';
@ -34,7 +35,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory<ContactCardE
constructor(
options: EmbeddableFactoryOptions<any>,
private readonly execTrigger: ExecuteTriggerActions,
private readonly execTrigger: TExecuteTriggerActions,
private readonly overlays: CoreStart['overlays']
) {
super(options);

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import { Container, EmbeddableFactory } from '../../..';
import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable';
import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory';
import { ExecuteTriggerActions } from '../../../types';
interface SlowContactCardEmbeddableFactoryOptions {
execAction: ExecuteTriggerActions;
execAction: TExecuteTriggerActions;
loadTickCount?: number;
}

View file

@ -54,6 +54,7 @@ export class FilterableContainer extends Container<
public getInheritedInput() {
return {
filters: this.input.filters,
viewMode: this.input.viewMode,
};
}

View file

@ -20,13 +20,10 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { CoreStart } from 'src/core/public';
import { TGetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public';
import { Container, ViewMode, ContainerInput } from '../..';
import { HelloWorldContainerComponent } from './hello_world_container_component';
import {
GetEmbeddableFactory,
GetActionsCompatibleWithTrigger,
GetEmbeddableFactories,
} from '../../types';
import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types';
import { Start as InspectorStartContract } from '../../../../../../../../../plugins/inspector/public';
export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER';
@ -48,7 +45,7 @@ interface HelloWorldContainerInput extends ContainerInput {
}
interface HelloWorldContainerOptions {
getActions: GetActionsCompatibleWithTrigger;
getActions: TGetActionsCompatibleWithTrigger;
getEmbeddableFactory: GetEmbeddableFactory;
getAllEmbeddableFactories: GetEmbeddableFactories;
overlays: CoreStart['overlays'];

View file

@ -21,17 +21,14 @@ import { Subscription } from 'rxjs';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { CoreStart } from 'src/core/public';
import { TGetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public';
import { IContainer, PanelState, EmbeddableChildPanel } from '../..';
import {
GetActionsCompatibleWithTrigger,
GetEmbeddableFactory,
GetEmbeddableFactories,
} from '../../types';
import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types';
import { Start as InspectorStartContract } from '../../../../../../../../../plugins/inspector/public';
interface Props {
container: IContainer;
getActions: GetActionsCompatibleWithTrigger;
getActions: TGetActionsCompatibleWithTrigger;
getEmbeddableFactory: GetEmbeddableFactory;
getAllEmbeddableFactories: GetEmbeddableFactories;
overlays: CoreStart['overlays'];

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { Action } from './actions';
import { EmbeddableFactory } from './embeddables/embeddable_factory';
import { Adapters } from '../../../../../../../plugins/inspector/public';
@ -53,10 +52,5 @@ export interface SavedObjectMetaData<T> {
showSavedObject?(savedObject: any): any;
}
export type ExecuteTriggerActions = <A>(triggerId: string, actionContext: A) => Promise<void>;
export type GetActionsCompatibleWithTrigger = <C>(
triggerId: string,
context: C
) => Promise<Action[]>;
export type GetEmbeddableFactory = (id: string) => EmbeddableFactory | undefined;
export type GetEmbeddableFactories = () => IterableIterator<EmbeddableFactory>;

View file

@ -20,39 +20,33 @@
import { Plugin } from '.';
import { coreMock } from '../../../../../../core/public/mocks';
// eslint-disable-next-line
import { uiActionsPluginMock } from '../../../../../../plugins/ui_actions/public/mocks';
export type Setup = jest.Mocked<ReturnType<Plugin['setup']>>;
export type Start = jest.Mocked<ReturnType<Plugin['start']>>;
const createSetupContract = (): Setup => {
const setupContract: Setup = {
attachAction: jest.fn(),
registerAction: jest.fn(),
registerEmbeddableFactory: jest.fn(),
registerTrigger: jest.fn(),
};
return setupContract;
};
const createStartContract = (): Start => {
const startContract: Start = {
attachAction: jest.fn(),
registerAction: jest.fn(),
registerEmbeddableFactory: jest.fn(),
registerTrigger: jest.fn(),
detachAction: jest.fn(),
executeTriggerActions: jest.fn(),
getEmbeddableFactories: jest.fn(),
getEmbeddableFactory: jest.fn(),
getTrigger: jest.fn(),
getTriggerActions: jest.fn(),
getTriggerCompatibleActions: jest.fn(),
};
return startContract;
};
const createInstance = () => {
const plugin = new Plugin({} as any);
const setup = plugin.setup(coreMock.createSetup());
const setup = plugin.setup(coreMock.createSetup(), {
uiActions: uiActionsPluginMock.createSetupContract(),
});
const doStart = () => plugin.start(coreMock.createStart());
return {
plugin,

View file

@ -17,39 +17,37 @@
* under the License.
*/
import { IUiActionsSetup } from 'src/plugins/ui_actions/public';
import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
} from '../../../../../../core/public';
import { TriggerRegistry, ActionRegistry, EmbeddableFactoryRegistry } from './types';
import { EmbeddableFactoryRegistry } from './types';
import { createApi, EmbeddableApi } from './api';
import { bootstrap } from './bootstrap';
export interface IEmbeddableSetupDependencies {
uiActions: IUiActionsSetup;
}
export class EmbeddablePublicPlugin implements Plugin<any, any> {
private readonly triggers: TriggerRegistry = new Map();
private readonly actions: ActionRegistry = new Map();
private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
private api!: EmbeddableApi;
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup) {
public setup(core: CoreSetup, { uiActions }: IEmbeddableSetupDependencies) {
({ api: this.api } = createApi({
actions: this.actions,
embeddableFactories: this.embeddableFactories,
triggers: this.triggers,
}));
bootstrap(this.api);
bootstrap(uiActions);
const { registerTrigger, registerAction, registerEmbeddableFactory, attachAction } = this.api;
const { registerEmbeddableFactory } = this.api;
return {
registerTrigger,
registerAction,
registerEmbeddableFactory,
attachAction,
};
}

View file

@ -18,7 +18,7 @@
*/
import { testPlugin } from './test_plugin';
import { ApplyFilterAction, EmbeddableOutput, isErrorEmbeddable } from '../lib';
import { EmbeddableOutput, isErrorEmbeddable, createFilterAction } from '../lib';
import {
FilterableContainer,
FilterableContainerInput,
@ -44,7 +44,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
api.registerEmbeddableFactory(factory1.type, factory1);
api.registerEmbeddableFactory(factory2.type, factory2);
const applyFilterAction = new ApplyFilterAction();
const applyFilterAction = createFilterAction();
const root = new FilterableContainer(
{ id: 'root', panels: {}, filters: [] },
@ -97,11 +97,11 @@ test('ApplyFilterAction is incompatible if the root container does not accept a
const api = doStart();
const inspector = inspectorPluginMock.createStartContract();
const applyFilterAction = new ApplyFilterAction();
const applyFilterAction = createFilterAction();
const parent = new HelloWorldContainer(
{ id: 'root', panels: {} },
{
getActions: api.getTriggerCompatibleActions,
getActions: () => Promise.resolve([]),
getEmbeddableFactory: api.getEmbeddableFactory,
getAllEmbeddableFactories: api.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -136,11 +136,11 @@ test('trying to execute on incompatible context throws an error ', async () => {
const factory = new FilterableEmbeddableFactory();
api.registerEmbeddableFactory(factory.type, factory);
const applyFilterAction = new ApplyFilterAction();
const applyFilterAction = createFilterAction();
const parent = new HelloWorldContainer(
{ id: 'root', panels: {} },
{
getActions: api.getTriggerCompatibleActions,
getActions: () => Promise.resolve([]),
getEmbeddableFactory: api.getEmbeddableFactory,
getAllEmbeddableFactories: api.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -161,13 +161,12 @@ test('trying to execute on incompatible context throws an error ', async () => {
}
async function check() {
// @ts-ignore
await applyFilterAction.execute({ embeddable });
await applyFilterAction.execute({ embeddable } as any);
}
await expect(check()).rejects.toThrow(Error);
});
test('gets title', async () => {
const applyFilterAction = new ApplyFilterAction();
expect(applyFilterAction.getDisplayName()).toBeDefined();
const applyFilterAction = createFilterAction();
expect(applyFilterAction.getDisplayName({} as any)).toBeDefined();
});

View file

@ -53,12 +53,12 @@ async function creatHelloWorldContainerAndEmbeddable(
) {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { setup, doStart } = testPlugin(coreSetup, coreStart);
const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart);
const start = doStart();
const filterableFactory = new FilterableEmbeddableFactory();
const slowContactCardFactory = new SlowContactCardEmbeddableFactory({
execAction: start.executeTriggerActions,
execAction: uiActions.executeTriggerActions,
});
const helloWorldFactory = new HelloWorldEmbeddableFactory();
@ -67,7 +67,7 @@ async function creatHelloWorldContainerAndEmbeddable(
setup.registerEmbeddableFactory(helloWorldFactory.type, helloWorldFactory);
const container = new HelloWorldContainer(containerInput, {
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
getAllEmbeddableFactories: start.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -85,7 +85,7 @@ async function creatHelloWorldContainerAndEmbeddable(
throw new Error('Error adding embeddable');
}
return { container, embeddable, coreSetup, coreStart, setup, start };
return { container, embeddable, coreSetup, coreStart, setup, start, uiActions };
}
test('Container initializes embeddables', async done => {
@ -128,7 +128,7 @@ test('Container.addNewEmbeddable', async () => {
});
test('Container.removeEmbeddable removes and cleans up', async done => {
const { start, coreStart } = await creatHelloWorldContainerAndEmbeddable();
const { start, coreStart, uiActions } = await creatHelloWorldContainerAndEmbeddable();
const container = new HelloWorldContainer(
{
id: 'hello',
@ -140,7 +140,7 @@ test('Container.removeEmbeddable removes and cleans up', async done => {
},
},
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
getAllEmbeddableFactories: start.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -291,7 +291,13 @@ test('Container view mode change propagates to children', async () => {
});
test(`Container updates its state when a child's input is updated`, async done => {
const { container, embeddable, start, coreStart } = await creatHelloWorldContainerAndEmbeddable(
const {
container,
embeddable,
start,
coreStart,
uiActions,
} = await creatHelloWorldContainerAndEmbeddable(
{ id: 'hello', panels: {}, viewMode: ViewMode.VIEW },
{
id: '123',
@ -314,7 +320,7 @@ test(`Container updates its state when a child's input is updated`, async done =
// with "Dr.", not the default the embeddable was first added with. Makes sure changed input
// is preserved with the container.
const containerClone = new HelloWorldContainer(container.getInput(), {
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getAllEmbeddableFactories: start.getEmbeddableFactories,
getEmbeddableFactory: start.getEmbeddableFactory,
notifications: coreStart.notifications,
@ -554,7 +560,7 @@ test('Panel added to input state', async () => {
test('Container changes made directly after adding a new embeddable are propagated', async done => {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const { doStart } = testPlugin(coreSetup, coreStart);
const { doStart, uiActions } = testPlugin(coreSetup, coreStart);
const start = doStart();
const container = new HelloWorldContainer(
@ -564,7 +570,7 @@ test('Container changes made directly after adding a new embeddable are propagat
viewMode: ViewMode.EDIT,
},
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
getAllEmbeddableFactories: start.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -576,7 +582,7 @@ test('Container changes made directly after adding a new embeddable are propagat
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: start.executeTriggerActions,
execAction: uiActions.executeTriggerActions,
});
start.registerEmbeddableFactory(factory.type, factory);
@ -685,7 +691,10 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in
});
test('untilEmbeddableLoaded() throws an error if there is no such child panel in the container - 2', async () => {
const { doStart, coreStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const { doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
const start = doStart();
const container = new HelloWorldContainer(
{
@ -693,7 +702,7 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in
panels: {},
},
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
getAllEmbeddableFactories: start.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -709,7 +718,10 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in
});
test('untilEmbeddableLoaded() resolves if child is loaded in the container', async done => {
const { setup, doStart, coreStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const { setup, doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
const factory = new HelloWorldEmbeddableFactory();
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
@ -724,7 +736,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy
},
},
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
getAllEmbeddableFactories: start.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -741,11 +753,14 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy
});
test('untilEmbeddableLoaded rejects with an error if child is subsequently removed', async done => {
const { doStart, coreStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const { doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
const start = doStart();
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: start.executeTriggerActions,
execAction: uiActions.executeTriggerActions,
});
start.registerEmbeddableFactory(factory.type, factory);
const container = new HelloWorldContainer(
@ -759,7 +774,7 @@ test('untilEmbeddableLoaded rejects with an error if child is subsequently remov
},
},
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
getAllEmbeddableFactories: start.getEmbeddableFactories,
overlays: coreStart.overlays,
@ -778,11 +793,14 @@ test('untilEmbeddableLoaded rejects with an error if child is subsequently remov
});
test('adding a panel then subsequently removing it before its loaded removes the panel', async done => {
const { doStart, coreStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const { doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
const start = doStart();
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 1,
execAction: start.executeTriggerActions,
execAction: uiActions.executeTriggerActions,
});
start.registerEmbeddableFactory(factory.type, factory);
const container = new HelloWorldContainer(
@ -796,7 +814,7 @@ test('adding a panel then subsequently removing it before its loaded removes the
},
},
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: start.getEmbeddableFactory,
getAllEmbeddableFactories: start.getEmbeddableFactories,
overlays: coreStart.overlays,

View file

@ -42,12 +42,15 @@ let container: Container;
let embeddable: ContactCardEmbeddable;
beforeEach(async () => {
const { doStart, coreStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const { doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
api = doStart();
const contactCardFactory = new ContactCardEmbeddableFactory(
{},
api.executeTriggerActions,
uiActions.executeTriggerActions,
{} as any
);
api.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory);
@ -55,7 +58,7 @@ beforeEach(async () => {
container = new HelloWorldContainer(
{ id: '123', panels: {} },
{
getActions: api.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: api.getEmbeddableFactory,
getAllEmbeddableFactories: api.getEmbeddableFactories,
overlays: coreStart.overlays,

View file

@ -35,12 +35,16 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world
// eslint-disable-next-line
import { coreMock } from '../../../../../../../core/public/mocks';
const { setup, doStart, coreStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const { setup, doStart, coreStart, uiActions } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
const start = doStart();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 2,
execAction: start.executeTriggerActions,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, factory);
setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory());
@ -76,7 +80,7 @@ test('Explicit embeddable input mapped to undefined with no inherited value will
const container = new HelloWorldContainer(
{ id: 'hello', panels: {} },
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getAllEmbeddableFactories: start.getEmbeddableFactories,
getEmbeddableFactory: start.getEmbeddableFactory,
notifications: coreStart.notifications,
@ -124,7 +128,7 @@ test('Explicit input tests in async situations', (done: () => void) => {
},
},
{
getActions: start.getTriggerCompatibleActions,
getActions: uiActions.getTriggerCompatibleActions,
getAllEmbeddableFactories: start.getEmbeddableFactories,
getEmbeddableFactory: start.getEmbeddableFactory,
notifications: coreStart.notifications,

View file

@ -18,6 +18,9 @@
*/
import { CoreSetup, CoreStart } from 'src/core/public';
// eslint-disable-next-line
import { uiActionsTestPlugin } from 'src/plugins/ui_actions/public/tests';
import { IUiActionsApi } from 'src/plugins/ui_actions/public';
import { EmbeddablePublicPlugin } from '../plugin';
export interface TestPluginReturn {
@ -26,15 +29,17 @@ export interface TestPluginReturn {
coreStart: CoreStart;
setup: ReturnType<EmbeddablePublicPlugin['setup']>;
doStart: (anotherCoreStart?: CoreStart) => ReturnType<EmbeddablePublicPlugin['start']>;
uiActions: IUiActionsApi;
}
export const testPlugin = (
coreSetup: CoreSetup = {} as CoreSetup,
coreStart: CoreStart = {} as CoreStart
): TestPluginReturn => {
const uiActions = uiActionsTestPlugin(coreSetup, coreStart);
const initializerContext = {} as any;
const plugin = new EmbeddablePublicPlugin(initializerContext);
const setup = plugin.setup(coreSetup);
const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup });
return {
plugin,
@ -45,5 +50,6 @@ export const testPlugin = (
const start = plugin.start(anotherCoreStart);
return start;
},
uiActions: uiActions.doStart(coreStart),
};
};

View file

@ -17,10 +17,6 @@
* under the License.
*/
import { Action } from './lib/actions';
import { EmbeddableFactory } from './lib/embeddables';
import { Trigger } from './lib/types';
export type TriggerRegistry = Map<string, Trigger>;
export type ActionRegistry = Map<string, Action>;
export type EmbeddableFactoryRegistry = Map<string, EmbeddableFactory>;

View file

@ -37,12 +37,12 @@ import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { timefilter, getTime } from 'ui/timefilter';
import { TimeRange } from 'src/plugins/data/public';
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import { Query, onlyDisabledFiltersChanged } from '../../../../data/public';
import {
APPLY_FILTER_TRIGGER,
Embeddable,
Container,
ExecuteTriggerActions,
} from '../../../../embeddable_api/public/np_ready/public';
import * as columnActions from '../doc_table/actions/columns';
import { SavedSearch } from '../types';
@ -124,7 +124,7 @@ export class SearchEmbeddable extends Embeddable<SearchInput, SearchOutput>
queryFilter,
}: SearchEmbeddableConfig,
initialInput: SearchInput,
private readonly executeTriggerActions: ExecuteTriggerActions,
private readonly executeTriggerActions: TExecuteTriggerActions,
parent?: Container
) {
super(

View file

@ -19,18 +19,19 @@
import '../doc_table';
import { capabilities } from 'ui/capabilities';
import { npStart } from 'ui/new_platform';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
import { IPrivate } from 'ui/private';
import { TimeRange } from 'src/plugins/data/public';
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public';
import {
EmbeddableFactory,
ErrorEmbeddable,
Container,
ExecuteTriggerActions,
} from '../../../../embeddable_api/public/np_ready/public';
import { setup, start } from '../../../../embeddable_api/public/np_ready/public/legacy';
import { setup } from '../../../../embeddable_api/public/np_ready/public/legacy';
import { SavedSearchLoader } from '../types';
import { SearchEmbeddable, SEARCH_EMBEDDABLE_TYPE } from './search_embeddable';
import { SearchInput, SearchOutput } from './types';
@ -42,7 +43,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory<
> {
public readonly type = SEARCH_EMBEDDABLE_TYPE;
constructor(private readonly executeTriggerActions: ExecuteTriggerActions) {
constructor(private readonly executeTriggerActions: TExecuteTriggerActions) {
super({
savedObjectMetaData: {
name: i18n.translate('kbn.discover.savedSearch.savedObjectName', {
@ -110,5 +111,5 @@ export class SearchEmbeddableFactory extends EmbeddableFactory<
}
}
const factory = new SearchEmbeddableFactory(start.executeTriggerActions);
const factory = new SearchEmbeddableFactory(npStart.plugins.uiActions.executeTriggerActions);
setup.registerEmbeddableFactory(factory.type, factory);

View file

@ -36,8 +36,6 @@ import {
EmbeddableOutput,
Embeddable,
Container,
APPLY_FILTER_TRIGGER,
Trigger,
} from '../../../../embeddable_api/public/np_ready/public';
import { Query, onlyDisabledFiltersChanged } from '../../../../data/public';
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
@ -130,10 +128,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
return this.handler.inspectorAdapters;
}
public supportsTrigger(trigger: Trigger) {
return trigger.id !== APPLY_FILTER_TRIGGER;
}
/**
* Transfers all changes in the containerState.customization into
* the uiState of this visualization.

View file

@ -22,6 +22,7 @@ import { coreMock } from '../../../../../core/public/mocks';
import { dataPluginMock } from '../../../../../plugins/data/public/mocks';
import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks';
import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks';
import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks';
/* eslint-enable @kbn/eslint/no-restricted-paths */
export const pluginsMock = {
@ -29,11 +30,13 @@ export const pluginsMock = {
data: dataPluginMock.createSetupContract(),
inspector: inspectorPluginMock.createSetupContract(),
expressions: expressionsPluginMock.createSetupContract(),
uiActions: uiActionsPluginMock.createSetupContract(),
}),
createStart: () => ({
data: dataPluginMock.createStartContract(),
inspector: inspectorPluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
uiActions: uiActionsPluginMock.createStartContract(),
}),
};

View file

@ -39,6 +39,11 @@ export const npSetup = {
},
},
},
uiActions: {
attachAction: sinon.fake(),
registerAction: sinon.fake(),
registerTrigger: sinon.fake(),
},
},
};
@ -60,6 +65,16 @@ export const npStart = {
close: () => Promise.resolve(undefined),
}),
},
uiActions: {
attachAction: sinon.fake(),
registerAction: sinon.fake(),
registerTrigger: sinon.fake(),
detachAction: sinon.fake(),
executeTriggerActions: sinon.fake(),
getTrigger: sinon.fake(),
getTriggerActions: sinon.fake(),
getTriggerCompatibleActions: sinon.fake(),
},
},
};

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public';
import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public';
import { Plugin as DataPlugin } from '../../../../plugins/data/public';
import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public';
@ -28,12 +29,14 @@ export interface PluginsSetup {
data: ReturnType<DataPlugin['setup']>;
expressions: ReturnType<ExpressionsPlugin['setup']>;
inspector: InspectorSetup;
uiActions: IUiActionsSetup;
}
export interface PluginsStart {
data: ReturnType<DataPlugin['start']>;
expressions: ReturnType<ExpressionsPlugin['start']>;
inspector: InspectorStart;
uiActions: IUiActionsStart;
}
export const npSetup = {

View file

@ -0,0 +1,10 @@
# UI Actions
An API for:
- creating custom functionality (`actions`)
- creating custom user interaction events (`triggers`)
- attaching and detaching `actions` to `triggers`.
- emitting `trigger` events
- executing `actions` attached to a given `trigger`.
- exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger.

View file

@ -0,0 +1,6 @@
{
"id": "uiActions",
"version": "kibana",
"server": false,
"ui": true
}

View file

@ -17,21 +17,17 @@
* under the License.
*/
import { SayHelloAction } from '../test_samples/actions/say_hello_action';
import { HelloWorldAction } from '../test_samples/actions/hello_world_action';
import { EmptyEmbeddable } from '../test_samples/embeddables/empty_embeddable';
import { createSayHelloAction } from '../tests/test_samples/say_hello_action';
test('SayHelloAction is not compatible with not matching embeddables', async () => {
const sayHelloAction = new SayHelloAction(() => {});
const emptyEmbeddable = new EmptyEmbeddable({ id: '234' });
test('SayHelloAction is not compatible with not matching context', async () => {
const sayHelloAction = createSayHelloAction((() => {}) as any);
const isCompatible = await sayHelloAction.isCompatible({ embeddable: emptyEmbeddable as any });
const isCompatible = await sayHelloAction.isCompatible({} as any);
expect(isCompatible).toBe(false);
});
test('HelloWorldAction inherits isCompatible from base action', async () => {
const helloWorldAction = new HelloWorldAction({} as any);
const emptyEmbeddable = new EmptyEmbeddable({ id: '234' });
const isCompatible = await helloWorldAction.isCompatible({ embeddable: emptyEmbeddable });
const helloWorldAction = createSayHelloAction({} as any);
const isCompatible = await helloWorldAction.isCompatible({ name: 'Sue' });
expect(isCompatible).toBe(true);
});

View file

@ -17,10 +17,20 @@
* under the License.
*/
import { CONTEXT_MENU_TRIGGER } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { setup } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
import { EditModeAction } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action';
import { IAction } from './i_action';
const editModeAction = new EditModeAction();
setup.registerAction(editModeAction);
setup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction.id);
export function createAction<ActionContext extends {} = {}>(
action: { type: string; execute: IAction<ActionContext>['execute'] } & Partial<
IAction<ActionContext>
>
): IAction<ActionContext> {
return {
getIconType: () => undefined,
order: 0,
id: action.type,
isCompatible: () => Promise.resolve(true),
getDisplayName: () => '',
getHref: () => undefined,
...action,
};
}

View file

@ -17,46 +17,41 @@
* under the License.
*/
export abstract class Action<ActionContext extends {} = {}> {
export interface IAction<ActionContext extends {} = {}> {
/**
* Determined the order when there is more than one action matched to a trigger.
* Higher numbers are displayed first.
*/
public order: number = 0;
order?: number;
public abstract readonly type: string;
constructor(public readonly id: string) {}
id: string;
readonly type: string;
/**
* Optional EUI icon type that can be displayed along with the title.
*/
public getIconType(context: ActionContext): string | undefined {
return undefined;
}
getIconType(context: ActionContext): string | undefined;
/**
* Returns a title to be displayed to the user.
* @param context
*/
public abstract getDisplayName(context: ActionContext): string;
getDisplayName(context: ActionContext): string;
/**
* Returns a promise that resolves to true if this action is compatible given the context,
* otherwise resolves to false.
*/
public async isCompatible(context: ActionContext): Promise<boolean> {
return true;
}
isCompatible(context: ActionContext): Promise<boolean>;
/**
* If this returns something truthy, this is used in addition to the `execute` method when clicked.
*/
public getHref(context: ActionContext): string | undefined {
return undefined;
}
getHref?(context: ActionContext): string | undefined;
/**
* Executes the action.
*/
public abstract async execute(context: ActionContext): Promise<void>;
execute(context: ActionContext): Promise<void>;
}

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
export { IAction } from './i_action';
export { createAction } from './create_action';

View file

@ -17,11 +17,11 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { IUiActionsApiPure } from '../types';
export const registerAction: EmbeddableApiPure['registerAction'] = ({ actions }) => action => {
export const registerAction: IUiActionsApiPure['registerAction'] = ({ actions }) => action => {
if (actions.has(action.id)) {
throw new Error(`Action [action.id = ${action.id}] already registered in Embeddables API.`);
throw new Error(`Action [action.id = ${action.id}] already registered.`);
}
actions.set(action.id, action);

View file

@ -0,0 +1,55 @@
/*
* 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 {
IUiActionsApi,
IUiActionsDependenciesInternal,
IUiActionsDependencies,
IUiActionsApiPure,
} from './types';
import { attachAction } from './triggers/attach_action';
import { detachAction } from './triggers/detach_action';
import { executeTriggerActions } from './triggers/execute_trigger_actions';
import { getTrigger } from './triggers/get_trigger';
import { getTriggerActions } from './triggers/get_trigger_actions';
import { getTriggerCompatibleActions } from './triggers/get_trigger_compatible_actions';
import { registerAction } from './actions/register_action';
import { registerTrigger } from './triggers/register_trigger';
export const pureApi: IUiActionsApiPure = {
attachAction,
detachAction,
executeTriggerActions,
getTrigger,
getTriggerActions,
getTriggerCompatibleActions,
registerAction,
registerTrigger,
};
export const createApi = (deps: IUiActionsDependencies) => {
const partialApi: Partial<IUiActionsApi> = {};
const depsInternal: IUiActionsDependenciesInternal = { ...deps, api: partialApi };
for (const [key, fn] of Object.entries(pureApi)) {
(partialApi as any)[key] = fn(depsInternal);
}
Object.freeze(partialApi);
const api = partialApi as IUiActionsApi;
return { api, depsInternal };
};

View file

@ -20,7 +20,7 @@
import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { Action } from '../actions';
import { IAction } from '../actions';
/**
* Transforms an array of Actions to the shape EuiContextMenuPanel expects.
@ -30,7 +30,7 @@ export async function buildContextMenuForActions<A>({
actionContext,
closeMenu,
}: {
actions: Array<Action<A>>;
actions: Array<IAction<A>>;
actionContext: A;
closeMenu: () => void;
}): Promise<EuiContextMenuPanelDescriptor> {
@ -42,7 +42,7 @@ export async function buildContextMenuForActions<A>({
return {
id: 'mainMenu',
title: i18n.translate('embeddableApi.actionPanel.title', {
title: i18n.translate('uiActions.actionPanel.title', {
defaultMessage: 'Options',
}),
items: menuItems,
@ -57,7 +57,7 @@ async function buildEuiContextMenuPanelItems<A>({
actionContext,
closeMenu,
}: {
actions: Array<Action<A>>;
actions: Array<IAction<A>>;
actionContext: A;
closeMenu: () => void;
}) {
@ -93,7 +93,7 @@ function convertPanelActionToContextMenuItem<A>({
actionContext,
closeMenu,
}: {
action: Action<A>;
action: IAction<A>;
actionContext: A;
closeMenu: () => void;
}): EuiContextMenuPanelItemDescriptor {
@ -109,7 +109,7 @@ function convertPanelActionToContextMenuItem<A>({
closeMenu();
};
if (action.getHref(actionContext)) {
if (action.getHref && action.getHref(actionContext)) {
menuPanelItem.href = action.getHref(actionContext);
}

View file

@ -0,0 +1,37 @@
/*
* 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 { PluginInitializerContext } from '../../../core/public';
import { UiActionsPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new UiActionsPlugin(initializerContext);
}
export { IUiActionsSetup, IUiActionsStart } from './plugin';
export {
IAction,
ITrigger,
IUiActionsApi,
TGetActionsCompatibleWithTrigger,
TExecuteTriggerActions,
} from './types';
export { createAction } from './actions';
export { buildContextMenuForActions } from './context_menu';
export { IncompatibleActionError } from './triggers';

View file

@ -0,0 +1,73 @@
/*
* 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 { IUiActionsSetup, IUiActionsStart } from '.';
import { plugin as pluginInitializer } from '.';
// eslint-disable-next-line
import { coreMock } from '../../../core/public/mocks';
export type Setup = jest.Mocked<IUiActionsSetup>;
export type Start = jest.Mocked<IUiActionsStart>;
const createSetupContract = (): Setup => {
const setupContract: Setup = {
attachAction: jest.fn(),
registerAction: jest.fn(),
registerTrigger: jest.fn(),
};
return setupContract;
};
const createStartContract = (): Start => {
const startContract: Start = {
attachAction: jest.fn(),
registerAction: jest.fn(),
registerTrigger: jest.fn(),
detachAction: jest.fn(),
executeTriggerActions: jest.fn(),
getTrigger: jest.fn(),
getTriggerActions: jest.fn((id: string) => []),
getTriggerCompatibleActions: jest.fn(),
};
return startContract;
};
const createPlugin = async () => {
const pluginInitializerContext = coreMock.createPluginInitializerContext();
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const plugin = pluginInitializer(pluginInitializerContext);
const setup = await plugin.setup(coreSetup);
return {
pluginInitializerContext,
coreSetup,
coreStart,
plugin,
setup,
doStart: async () => await plugin.start(coreStart),
};
};
export const uiActionsPluginMock = {
createSetupContract,
createStartContract,
createPlugin,
};

View file

@ -0,0 +1,54 @@
/*
* 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 { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public';
import { IUiActionsApi, IActionRegistry, ITriggerRegistry } from './types';
import { createApi } from './api';
export interface IUiActionsSetup {
attachAction: IUiActionsApi['attachAction'];
registerAction: IUiActionsApi['registerAction'];
registerTrigger: IUiActionsApi['registerTrigger'];
}
export type IUiActionsStart = IUiActionsApi;
export class UiActionsPlugin implements Plugin<IUiActionsSetup, IUiActionsStart> {
private readonly triggers: ITriggerRegistry = new Map();
private readonly actions: IActionRegistry = new Map();
private api!: IUiActionsApi;
constructor(initializerContext: PluginInitializerContext) {
this.api = createApi({ triggers: this.triggers, actions: this.actions }).api;
}
public setup(core: CoreSetup): IUiActionsSetup {
return {
registerTrigger: this.api.registerTrigger,
registerAction: this.api.registerAction,
attachAction: this.api.attachAction,
};
}
public start(core: CoreStart): IUiActionsStart {
return this.api;
}
public stop() {}
}

View file

@ -0,0 +1,28 @@
/*
* 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 { IUiActionsDependencies } from '../types';
export const createDeps = (): IUiActionsDependencies => {
const deps: IUiActionsDependencies = {
actions: new Map<any, any>(),
triggers: new Map<any, any>(),
};
return deps;
};

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { uiActionsTestPlugin } from './test_plugin';

View file

@ -0,0 +1,49 @@
/*
* 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 { CoreSetup, CoreStart } from 'src/core/public';
import { UiActionsPlugin, IUiActionsSetup, IUiActionsStart } from '../plugin';
export interface IUiActionsTestPluginReturn {
plugin: UiActionsPlugin;
coreSetup: CoreSetup;
coreStart: CoreStart;
setup: IUiActionsSetup;
doStart: (anotherCoreStart?: CoreStart) => IUiActionsStart;
}
export const uiActionsTestPlugin = (
coreSetup: CoreSetup = {} as CoreSetup,
coreStart: CoreStart = {} as CoreStart
): IUiActionsTestPluginReturn => {
const initializerContext = {} as any;
const plugin = new UiActionsPlugin(initializerContext);
const setup = plugin.setup(coreSetup);
return {
plugin,
coreSetup,
coreStart,
setup,
doStart: (anotherCoreStart: CoreStart = coreStart) => {
const start = plugin.start(anotherCoreStart);
return start;
},
};
};

View file

@ -19,30 +19,23 @@
import React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { CoreStart } from '../../../../../../../../../core/public';
import { Action } from '../..';
import { CoreStart } from 'src/core/public';
import { createAction, IAction } from '../../actions';
export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
export class HelloWorldAction extends Action {
public readonly type = HELLO_WORLD_ACTION_ID;
constructor(private readonly overlays: CoreStart['overlays']) {
super(HELLO_WORLD_ACTION_ID);
}
public getDisplayName() {
return 'Hello World Action!';
}
public async execute() {
const flyoutSession = this.overlays.openFlyout(
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
Hello World, I am a hello world action!
</EuiFlyout>,
{
'data-test-subj': 'helloWorldAction',
}
);
}
export function createHelloWorldAction(overlays: CoreStart['overlays']): IAction {
return createAction({
type: HELLO_WORLD_ACTION_ID,
execute: async () => {
const flyoutSession = overlays.openFlyout(
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
Hello World, I am a hello world action!
</EuiFlyout>,
{
'data-test-subj': 'helloWorldAction',
}
);
},
});
}

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
export { createRestrictedAction } from './restricted_action';
export { createSayHelloAction } from './say_hello_action';
export { createHelloWorldAction } from './hello_world_action';

View file

@ -17,26 +17,14 @@
* under the License.
*/
import { Action } from '../../actions';
import { IAction, createAction } from '../../actions';
export const RESTRICTED_ACTION = 'RESTRICTED_ACTION';
export class RestrictedAction<A> extends Action<A> {
public readonly type = RESTRICTED_ACTION;
private isCompatibleFn: (context: A) => boolean;
constructor(isCompatible: (context: A) => boolean) {
super(RESTRICTED_ACTION);
this.isCompatibleFn = isCompatible;
}
getDisplayName() {
return `I am only sometimes compatible`;
}
async isCompatible(context: A) {
return this.isCompatibleFn(context);
}
async execute() {}
export function createRestrictedAction<C>(isCompatibleIn: (context: C) => boolean): IAction<C> {
return createAction<C>({
type: RESTRICTED_ACTION,
isCompatible: async context => isCompatibleIn(context),
execute: async () => {},
});
}

View file

@ -0,0 +1,43 @@
/*
* 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 { IAction, createAction } from '../../actions';
export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION';
export function createSayHelloAction(overlays: CoreStart['overlays']): IAction<{ 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(
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
this.getDisplayName(context)
</EuiFlyout>,
{
'data-test-subj': 'sayHelloAction',
}
);
},
});
}

View file

@ -17,9 +17,9 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { IUiActionsApiPure } from '../types';
export const attachAction: EmbeddableApiPure['attachAction'] = ({ triggers }) => (
export const attachAction: IUiActionsApiPure['attachAction'] = ({ triggers }) => (
triggerId,
actionId
) => {

View file

@ -17,9 +17,9 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { IUiActionsApiPure } from '../types';
export const detachAction: EmbeddableApiPure['detachAction'] = ({ triggers }) => (
export const detachAction: IUiActionsApiPure['detachAction'] = ({ triggers }) => (
triggerId,
actionId
) => {

View file

@ -17,49 +17,33 @@
* under the License.
*/
import { testPlugin, TestPluginReturn } from './test_plugin';
import { of } from './helpers';
import { Action, openContextMenu, IEmbeddable } from '../lib';
import {
ContactCardEmbeddable,
CONTACT_USER_TRIGGER,
} from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable';
import { SEND_MESSAGE_ACTION } from '../lib/test_samples/actions/send_message_action';
import { IAction, createAction } from '../actions';
import { openContextMenu } from '../context_menu';
import { IUiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin';
jest.mock('../lib/context_menu_actions');
jest.mock('../context_menu');
const executeFn = jest.fn();
const openContextMenuSpy = (openContextMenu as any) as jest.SpyInstance;
class TestAction<A> extends Action<A> {
public readonly type = 'testAction';
public checkCompatibility: (context: A) => boolean;
const CONTACT_USER_TRIGGER = 'CONTACT_USER_TRIGGER';
constructor(id: string, checkCompatibility: (context: A) => boolean) {
super(id);
this.checkCompatibility = checkCompatibility;
}
public getDisplayName() {
return 'test';
}
async isCompatible(context: A) {
return this.checkCompatibility(context);
}
async execute(context: unknown) {
executeFn(context);
}
function createTestAction<A>(id: string, checkCompatibility: (context: A) => boolean): IAction<A> {
return createAction<A>({
type: 'testAction',
id,
isCompatible: context => Promise.resolve(checkCompatibility(context)),
execute: context => executeFn(context),
});
}
let embeddables: TestPluginReturn;
let uiActions: IUiActionsTestPluginReturn;
const reset = () => {
embeddables = testPlugin();
uiActions = uiActionsTestPlugin();
embeddables.setup.registerTrigger({
uiActions.setup.registerTrigger({
id: CONTACT_USER_TRIGGER,
actionIds: [SEND_MESSAGE_ACTION],
actionIds: ['SEND_MESSAGE_ACTION'],
});
executeFn.mockReset();
@ -68,23 +52,17 @@ const reset = () => {
beforeEach(reset);
test('executes a single action mapped to a trigger', async () => {
const { setup, doStart } = embeddables;
const { setup, doStart } = uiActions;
const trigger = {
id: 'MY-TRIGGER',
title: 'My trigger',
actionIds: ['test1'],
};
const action = new TestAction('test1', () => true);
const action = createTestAction('test1', () => true);
setup.registerTrigger(trigger);
setup.registerAction(action);
const context = {
embeddable: new ContactCardEmbeddable(
{ id: '123', firstName: 'Stacey', lastName: 'G' },
{ execAction: (() => null) as any }
),
triggerContext: {},
};
const context = {};
const start = doStart();
await start.executeTriggerActions('MY-TRIGGER', context);
@ -93,7 +71,7 @@ test('executes a single action mapped to a trigger', async () => {
});
test('throws an error if there are no compatible actions to execute', async () => {
const { setup, doStart } = embeddables;
const { setup, doStart } = uiActions;
const trigger = {
id: 'MY-TRIGGER',
title: 'My trigger',
@ -101,48 +79,27 @@ test('throws an error if there are no compatible actions to execute', async () =
};
setup.registerTrigger(trigger);
const context = {
embeddable: new ContactCardEmbeddable(
{ id: '123', firstName: 'Stacey', lastName: 'G' },
{ execAction: (() => null) as any }
),
triggerContext: {},
};
const context = {};
const start = doStart();
const [, error] = await of(start.executeTriggerActions('MY-TRIGGER', context));
expect(error).toBeInstanceOf(Error);
expect(error.message).toMatchInlineSnapshot(
`"No compatible actions found to execute for trigger [triggerId = MY-TRIGGER]."`
await expect(start.executeTriggerActions('MY-TRIGGER', context)).rejects.toMatchObject(
new Error('No compatible actions found to execute for trigger [triggerId = MY-TRIGGER].')
);
});
test('does not execute an incompatible action', async () => {
const { setup, doStart } = embeddables;
const { setup, doStart } = uiActions;
const trigger = {
id: 'MY-TRIGGER',
title: 'My trigger',
actionIds: ['test1'],
};
const action = new TestAction<{ embeddable: IEmbeddable }>(
'test1',
({ embeddable }) => embeddable.id === 'executeme'
);
const embeddable = new ContactCardEmbeddable(
{
id: 'executeme',
firstName: 'Stacey',
lastName: 'G',
},
{} as any
);
const action = createTestAction<{ name: string }>('test1', ({ name }) => name === 'executeme');
setup.registerTrigger(trigger);
setup.registerAction(action);
const start = doStart();
const context = {
embeddable,
triggerContext: {},
name: 'executeme',
};
await start.executeTriggerActions('MY-TRIGGER', context);
@ -150,22 +107,14 @@ test('does not execute an incompatible action', async () => {
});
test('shows a context menu when more than one action is mapped to a trigger', async () => {
const { setup, doStart } = embeddables;
const { setup, doStart } = uiActions;
const trigger = {
id: 'MY-TRIGGER',
title: 'My trigger',
actionIds: ['test1', 'test2'],
};
const action1 = new TestAction('test1', () => true);
const action2 = new TestAction('test2', () => true);
const embeddable = new ContactCardEmbeddable(
{
id: 'executeme',
firstName: 'Stacey',
lastName: 'G',
},
{} as any
);
const action1 = createTestAction('test1', () => true);
const action2 = createTestAction('test2', () => true);
setup.registerTrigger(trigger);
setup.registerAction(action1);
setup.registerAction(action2);
@ -173,10 +122,7 @@ test('shows a context menu when more than one action is mapped to a trigger', as
expect(openContextMenu).toHaveBeenCalledTimes(0);
const start = doStart();
const context = {
embeddable,
triggerContext: {},
};
const context = {};
await start.executeTriggerActions('MY-TRIGGER', context);
expect(executeFn).toBeCalledTimes(0);
@ -184,16 +130,14 @@ test('shows a context menu when more than one action is mapped to a trigger', as
});
test('passes whole action context to isCompatible()', async () => {
const { setup, doStart } = embeddables;
const { setup, doStart } = uiActions;
const trigger = {
id: 'MY-TRIGGER',
title: 'My trigger',
actionIds: ['test'],
};
const action = new TestAction<{ triggerContext: any }>('test', ({ triggerContext }) => {
expect(triggerContext).toEqual({
foo: 'bar',
});
const action = createTestAction<{ foo: string }>('test', ({ foo }) => {
expect(foo).toEqual('bar');
return true;
});
@ -201,11 +145,6 @@ test('passes whole action context to isCompatible()', async () => {
setup.registerAction(action);
const start = doStart();
const context = {
embeddable: {} as any,
triggerContext: {
foo: 'bar',
},
};
const context = { foo: 'bar' };
await start.executeTriggerActions('MY-TRIGGER', context);
});

View file

@ -17,11 +17,12 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { Action, buildContextMenuForActions, openContextMenu } from '../lib';
import { IUiActionsApiPure } from '../types';
import { buildContextMenuForActions, openContextMenu } from '../context_menu';
import { IAction } from '../actions';
const executeSingleAction = async <A extends {} = {}>(action: Action<A>, actionContext: A) => {
const href = action.getHref(actionContext);
const executeSingleAction = async <A extends {} = {}>(action: IAction<A>, actionContext: A) => {
const href = action.getHref && action.getHref(actionContext);
// TODO: Do we need a `getHref()` special case?
if (href) {
@ -32,7 +33,7 @@ const executeSingleAction = async <A extends {} = {}>(action: Action<A>, actionC
await action.execute(actionContext);
};
export const executeTriggerActions: EmbeddableApiPure['executeTriggerActions'] = ({
export const executeTriggerActions: IUiActionsApiPure['executeTriggerActions'] = ({
api,
}) => async (triggerId, actionContext) => {
const actions = await api.getTriggerCompatibleActions!(triggerId, actionContext);

View file

@ -17,9 +17,8 @@
* under the License.
*/
import { createApi } from '..';
import { createDeps } from './helpers';
import { expectError } from '../../tests/helpers';
import { createApi } from '../api';
import { createDeps } from '../tests/helpers';
test('can get Trigger from registry', () => {
const deps = createDeps();
@ -45,8 +44,5 @@ test('throws if trigger does not exist', () => {
const deps = createDeps();
const { api } = createApi(deps);
const error = expectError(() => api.getTrigger('foo'));
expect(error).toBeInstanceOf(Error);
expect(error.message).toMatchInlineSnapshot(`"Trigger [triggerId = foo] does not exist."`);
expect(() => api.getTrigger('foo')).toThrowError('Trigger [triggerId = foo] does not exist.');
});

View file

@ -17,9 +17,9 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { IUiActionsApiPure } from '../types';
export const getTrigger: EmbeddableApiPure['getTrigger'] = ({ triggers }) => id => {
export const getTrigger: IUiActionsApiPure['getTrigger'] = ({ triggers }) => id => {
const trigger = triggers.get(id);
if (!trigger) {

View file

@ -17,22 +17,22 @@
* under the License.
*/
import { testPlugin } from './test_plugin';
import { Action } from '../lib';
import { IAction } from '../actions';
import { uiActionsTestPlugin } from '../tests/test_plugin';
const action1 = ({
const action1: IAction = {
id: 'action1',
order: 1,
type: 'type1',
} as any) as Action;
const action2 = ({
} as any;
const action2: IAction = {
id: 'action2',
order: 2,
type: 'type2',
} as any) as Action;
} as any;
test('returns actions set on trigger', () => {
const { setup, doStart } = testPlugin();
const { setup, doStart } = uiActionsTestPlugin();
setup.registerAction(action1);
setup.registerAction(action2);
setup.registerTrigger({

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { Action } from '../lib';
import { IUiActionsApiPure } from '../types';
import { IAction } from '../actions';
export const getTriggerActions: EmbeddableApiPure['getTriggerActions'] = ({
export const getTriggerActions: IUiActionsApiPure['getTriggerActions'] = ({
api,
actions,
}) => id => {
const trigger = api.getTrigger!(id);
return trigger.actionIds.map(actionId => actions.get(actionId)).filter(Boolean) as Action[];
return trigger.actionIds.map(actionId => actions.get(actionId)).filter(Boolean) as IAction[];
};

View file

@ -17,27 +17,29 @@
* under the License.
*/
import { testPlugin, TestPluginReturn } from './test_plugin';
import { HelloWorldAction } from '../lib/test_samples/actions/hello_world_action';
import { SayHelloAction } from '../lib/test_samples/actions/say_hello_action';
import { RestrictedAction } from '../lib/test_samples/actions/restricted_action';
import { EmptyEmbeddable } from '../lib/test_samples/embeddables/empty_embeddable';
import { CONTEXT_MENU_TRIGGER, IEmbeddable } from '../lib';
import { of } from './helpers';
import { createSayHelloAction } from '../tests/test_samples/say_hello_action';
import { IUiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin';
import { createRestrictedAction, createHelloWorldAction } from '../tests/test_samples';
import { IAction } from '../actions';
let action: SayHelloAction;
let embeddables: TestPluginReturn;
let action: IAction<{ name: string }>;
let uiActions: IUiActionsTestPluginReturn;
beforeEach(() => {
embeddables = testPlugin();
action = new SayHelloAction(() => {});
uiActions = uiActionsTestPlugin();
action = createSayHelloAction({} as any);
embeddables.setup.registerAction(action);
embeddables.setup.attachAction(CONTEXT_MENU_TRIGGER, action.id);
uiActions.setup.registerAction(action);
uiActions.setup.registerTrigger({
id: 'trigger',
title: 'trigger',
actionIds: [],
});
uiActions.setup.attachAction('trigger', action.id);
});
test('can register and get actions', async () => {
const { setup, plugin } = embeddables;
const helloWorldAction = new HelloWorldAction({} as any);
const { setup, plugin } = uiActions;
const helloWorldAction = createHelloWorldAction({} as any);
const length = (plugin as any).actions.size;
setup.registerAction(helloWorldAction);
@ -48,9 +50,8 @@ test('can register and get actions', async () => {
});
test('getTriggerCompatibleActions returns attached actions', async () => {
const { setup, doStart } = embeddables;
const embeddable = new EmptyEmbeddable({ id: '123' });
const helloWorldAction = new HelloWorldAction({} as any);
const { setup, doStart } = uiActions;
const helloWorldAction = createHelloWorldAction({} as any);
setup.registerAction(helloWorldAction);
@ -63,25 +64,20 @@ test('getTriggerCompatibleActions returns attached actions', async () => {
setup.attachAction('MY-TRIGGER', helloWorldAction.id);
const start = doStart();
const actions = await start.getTriggerCompatibleActions('MY-TRIGGER', {
embeddable,
});
const actions = await start.getTriggerCompatibleActions('MY-TRIGGER', {});
expect(actions.length).toBe(1);
expect(actions[0].id).toBe(helloWorldAction.id);
});
test('filters out actions not applicable based on the context', async () => {
const { setup, doStart } = embeddables;
const restrictedAction = new RestrictedAction<{ embeddable: IEmbeddable }>(context => {
return context.embeddable.id === 'accept';
const { setup, doStart } = uiActions;
const restrictedAction = createRestrictedAction<{ accept: boolean }>(context => {
return context.accept;
});
setup.registerAction(restrictedAction);
const acceptEmbeddable = new EmptyEmbeddable({ id: 'accept' });
const rejectEmbeddable = new EmptyEmbeddable({ id: 'reject' });
const testTrigger = {
id: 'MY-TRIGGER',
title: 'My trigger',
@ -91,36 +87,26 @@ test('filters out actions not applicable based on the context', async () => {
setup.registerTrigger(testTrigger);
const start = doStart();
let actions = await start.getTriggerCompatibleActions(testTrigger.id, {
embeddable: acceptEmbeddable,
});
let actions = await start.getTriggerCompatibleActions(testTrigger.id, { accept: true });
expect(actions.length).toBe(1);
actions = await start.getTriggerCompatibleActions(testTrigger.id, {
embeddable: rejectEmbeddable,
});
actions = await start.getTriggerCompatibleActions(testTrigger.id, { accept: false });
expect(actions.length).toBe(0);
});
test(`throws an error with an invalid trigger ID`, async () => {
const { doStart } = embeddables;
const { doStart } = uiActions;
const start = doStart();
const [, error] = await of(
start.getTriggerCompatibleActions('I do not exist', {
embeddable: new EmptyEmbeddable({ id: 'empty' }),
})
);
await expect(error).toBeInstanceOf(Error);
await expect(error.message).toMatchInlineSnapshot(
`"Trigger [triggerId = I do not exist] does not exist."`
await expect(start.getTriggerCompatibleActions('I do not exist', {})).rejects.toMatchObject(
new Error('Trigger [triggerId = I do not exist] does not exist.')
);
});
test(`with a trigger mapping that maps to an non-existing action returns empty list`, async () => {
const { setup, doStart } = embeddables;
const { setup, doStart } = uiActions;
const testTrigger = {
id: '123',
title: '123',
@ -129,9 +115,7 @@ test(`with a trigger mapping that maps to an non-existing action returns empty l
setup.registerTrigger(testTrigger);
const start = doStart();
const actions = await start.getTriggerCompatibleActions(testTrigger.id, {
embeddable: new EmptyEmbeddable({ id: 'empty' }),
});
const actions = await start.getTriggerCompatibleActions(testTrigger.id, {});
expect(actions).toEqual([]);
});

View file

@ -17,15 +17,15 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { Action } from '../lib';
import { IUiActionsApiPure } from '../types';
import { IAction } from '../actions/i_action';
export const getTriggerCompatibleActions: EmbeddableApiPure['getTriggerCompatibleActions'] = ({
export const getTriggerCompatibleActions: IUiActionsApiPure['getTriggerCompatibleActions'] = ({
api,
}) => async (triggerId, context) => {
const actions = api.getTriggerActions!(triggerId);
const isCompatibles = await Promise.all(actions.map(action => action.isCompatible(context)));
return actions.reduce<Action[]>(
return actions.reduce<IAction[]>(
(acc, action, i) => (isCompatibles[i] ? [...acc, action] : acc),
[]
);

View file

@ -0,0 +1,25 @@
/*
* 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.
*/
export interface ITrigger {
id: string;
title?: string;
description?: string;
actionIds: string[];
}

View file

@ -0,0 +1,31 @@
/*
* 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 { i18n } from '@kbn/i18n';
export class IncompatibleActionError extends Error {
code = 'INCOMPATIBLE_ACTION';
constructor() {
super(
i18n.translate('uiActions.errors.incompatibleAction', {
defaultMessage: 'Action is incompatible',
})
);
}
}

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { IncompatibleActionError } from './incompatible_action_error';

View file

@ -17,11 +17,11 @@
* under the License.
*/
import { EmbeddableApiPure } from './types';
import { IUiActionsApiPure } from '../types';
export const registerTrigger: EmbeddableApiPure['registerTrigger'] = ({ triggers }) => trigger => {
export const registerTrigger: IUiActionsApiPure['registerTrigger'] = ({ triggers }) => trigger => {
if (triggers.has(trigger.id)) {
throw new Error(`Trigger [trigger.id = ${trigger.id}] already registered in Embeddables API.`);
throw new Error(`Trigger [trigger.id = ${trigger.id}] already registered.`);
}
triggers.set(trigger.id, trigger);

View file

@ -0,0 +1,149 @@
/*
* 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 { createApi } from '../api';
import { createDeps } from '../tests/helpers';
const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
test('can register trigger', () => {
const deps = createDeps();
const { api } = createApi(deps);
api.registerTrigger({
actionIds: [],
description: 'foo',
id: 'bar',
title: 'baz',
});
expect(deps.triggers.get('bar')).toEqual({
actionIds: [],
description: 'foo',
id: 'bar',
title: 'baz',
});
});
test('can register action', () => {
const deps = createDeps();
const { api } = createApi(deps);
api.registerAction({
id: HELLO_WORLD_ACTION_ID,
order: 13,
} as any);
expect(deps.actions.get(HELLO_WORLD_ACTION_ID)).toMatchObject({
id: HELLO_WORLD_ACTION_ID,
order: 13,
});
});
test('can attach an action to a trigger', () => {
const deps = createDeps();
const { api } = createApi(deps);
const trigger = {
id: 'MY-TRIGGER',
actionIds: [],
};
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
expect(trigger.actionIds).toEqual([]);
api.registerTrigger(trigger);
api.registerAction(action);
api.attachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
expect(trigger.actionIds).toEqual([HELLO_WORLD_ACTION_ID]);
});
test('can detach an action to a trigger', () => {
const deps = createDeps();
const { api } = createApi(deps);
const trigger = {
id: 'MY-TRIGGER',
actionIds: [],
};
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
expect(trigger.actionIds).toEqual([]);
api.registerTrigger(trigger);
api.registerAction(action);
api.attachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
api.detachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
expect(trigger.actionIds).toEqual([]);
});
test('detaching an invalid action from a trigger throws an error', async () => {
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
api.registerAction(action);
expect(() => api.detachAction('i do not exist', HELLO_WORLD_ACTION_ID)).toThrowError(
'No trigger [triggerId = i do not exist] exists, for detaching action [actionId = HELLO_WORLD_ACTION_ID].'
);
});
test('attaching an invalid action to a trigger throws an error', async () => {
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
api.registerAction(action);
expect(() => api.attachAction('i do not exist', HELLO_WORLD_ACTION_ID)).toThrowError(
'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = HELLO_WORLD_ACTION_ID].'
);
});
test('cannot register another action with the same ID', async () => {
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
const action = {
id: HELLO_WORLD_ACTION_ID,
order: 25,
} as any;
api.registerAction(action);
expect(() => api.registerAction(action)).toThrowError(
'Action [action.id = HELLO_WORLD_ACTION_ID] already registered.'
);
});
test('cannot register another trigger with the same ID', async () => {
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
const trigger = { id: 'MY-TRIGGER' } as any;
api.registerTrigger(trigger);
expect(() => api.registerTrigger(trigger)).toThrowError(
'Trigger [trigger.id = MY-TRIGGER] already registered.'
);
});

View file

@ -0,0 +1,58 @@
/*
* 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 { IAction } from './actions/i_action';
import { ITrigger } from './triggers/i_trigger';
export { IAction } from './actions';
export { ITrigger } from './triggers/i_trigger';
export type TExecuteTriggerActions = <A>(triggerId: string, actionContext: A) => Promise<void>;
export type TGetActionsCompatibleWithTrigger = <C>(
triggerId: string,
context: C
) => Promise<IAction[]>;
export interface IUiActionsApi {
attachAction: (triggerId: string, actionId: string) => void;
detachAction: (triggerId: string, actionId: string) => void;
executeTriggerActions: TExecuteTriggerActions;
getTrigger: (id: string) => ITrigger;
getTriggerActions: (id: string) => IAction[];
getTriggerCompatibleActions: <C>(triggerId: string, context: C) => Promise<Array<IAction<C>>>;
registerAction: (action: IAction) => void;
registerTrigger: (trigger: ITrigger) => void;
}
export interface IUiActionsDependencies {
actions: IActionRegistry;
triggers: ITriggerRegistry;
}
export interface IUiActionsDependenciesInternal extends IUiActionsDependencies {
api: Readonly<Partial<IUiActionsApi>>;
}
export type IUiActionsApiPure = {
[K in keyof IUiActionsApi]: (deps: IUiActionsDependenciesInternal) => IUiActionsApi[K];
};
export type ITriggerRegistry = Map<string, ITrigger>;
export type IActionRegistry = Map<string, IAction>;

Some files were not shown because too many files have changed in this diff Show more