mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Decouple actions from embeddables: step 1 * prefer as any instead of is-ignore * Remove unneccessary test, no more triggerContext to be null. * Fix bug and fix the test that should have caught it. Be more strict about checking isCompatible.
This commit is contained in:
parent
f49c6bf487
commit
531534659a
37 changed files with 231 additions and 262 deletions
|
@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n';
|
|||
import {
|
||||
Action,
|
||||
IEmbeddable,
|
||||
ActionContext,
|
||||
IncompatibleActionError,
|
||||
} from '../../../../../../embeddable_api/public/np_ready/public';
|
||||
import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable';
|
||||
|
@ -40,7 +39,11 @@ function isExpanded(embeddable: IEmbeddable) {
|
|||
return embeddable.id === embeddable.parent.getInput().expandedPanelId;
|
||||
}
|
||||
|
||||
export class ExpandPanelAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class ExpandPanelAction extends Action<ActionContext> {
|
||||
public readonly type = EXPAND_PANEL_ACTION;
|
||||
|
||||
constructor() {
|
||||
|
@ -80,7 +83,7 @@ export class ExpandPanelAction extends Action {
|
|||
return Boolean(embeddable.parent && isDashboard(embeddable.parent));
|
||||
}
|
||||
|
||||
public execute({ embeddable }: ActionContext) {
|
||||
public async execute({ embeddable }: ActionContext) {
|
||||
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
*/
|
||||
|
||||
import { EmbeddableApiPure } from './types';
|
||||
import { Action, ActionContext, buildContextMenuForActions, openContextMenu } from '../lib';
|
||||
import { Action, buildContextMenuForActions, openContextMenu } from '../lib';
|
||||
|
||||
const executeSingleAction = async (action: Action, actionContext: ActionContext) => {
|
||||
const executeSingleAction = async <A extends {} = {}>(action: Action<A>, actionContext: A) => {
|
||||
const href = action.getHref(actionContext);
|
||||
|
||||
// TODO: Do we need a `getHref()` special case?
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
EmbeddableFactory,
|
||||
ExecuteTriggerActions,
|
||||
GetEmbeddableFactories,
|
||||
TriggerContext,
|
||||
} from '../lib';
|
||||
|
||||
export interface EmbeddableApi {
|
||||
|
@ -35,7 +34,7 @@ export interface EmbeddableApi {
|
|||
getEmbeddableFactories: GetEmbeddableFactories;
|
||||
getTrigger: (id: string) => Trigger;
|
||||
getTriggerActions: (id: string) => Action[];
|
||||
getTriggerCompatibleActions: (triggerId: string, context: TriggerContext) => Promise<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;
|
||||
|
|
|
@ -26,7 +26,6 @@ export {
|
|||
APPLY_FILTER_TRIGGER,
|
||||
PANEL_BADGE_TRIGGER,
|
||||
Action,
|
||||
ActionContext,
|
||||
Adapters,
|
||||
AddPanelAction,
|
||||
ApplyFilterAction,
|
||||
|
@ -59,7 +58,6 @@ export {
|
|||
PropertySpec,
|
||||
SavedObjectMetaData,
|
||||
Trigger,
|
||||
TriggerContext,
|
||||
ViewMode,
|
||||
isErrorEmbeddable,
|
||||
openAddPanelFlyout,
|
||||
|
|
|
@ -17,20 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { IEmbeddable } from '../embeddables';
|
||||
|
||||
export interface ActionContext<
|
||||
TEmbeddable extends IEmbeddable = IEmbeddable,
|
||||
TTriggerContext extends {} = {}
|
||||
> {
|
||||
embeddable: TEmbeddable;
|
||||
triggerContext?: TTriggerContext;
|
||||
}
|
||||
|
||||
export abstract class Action<
|
||||
TEmbeddable extends IEmbeddable = IEmbeddable,
|
||||
TTriggerContext extends {} = {}
|
||||
> {
|
||||
export abstract class Action<ActionContext extends {} = {}> {
|
||||
/**
|
||||
* Determined the order when there is more than one action matched to a trigger.
|
||||
* Higher numbers are displayed first.
|
||||
|
@ -43,7 +30,7 @@ export abstract class Action<
|
|||
/**
|
||||
* Optional EUI icon type that can be displayed along with the title.
|
||||
*/
|
||||
public getIconType(context: ActionContext<TEmbeddable, TTriggerContext>): string | undefined {
|
||||
public getIconType(context: ActionContext): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -51,27 +38,25 @@ export abstract class Action<
|
|||
* Returns a title to be displayed to the user.
|
||||
* @param context
|
||||
*/
|
||||
public abstract getDisplayName(context: ActionContext<TEmbeddable, TTriggerContext>): string;
|
||||
public abstract 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<TEmbeddable, TTriggerContext>
|
||||
): Promise<boolean> {
|
||||
public async isCompatible(context: ActionContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this returns something truthy, this is used in addition to the `execute` method when clicked.
|
||||
*/
|
||||
public getHref(context: ActionContext<TEmbeddable, TTriggerContext>): string | undefined {
|
||||
public getHref(context: ActionContext): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the action.
|
||||
*/
|
||||
public abstract execute(context: ActionContext<TEmbeddable, TTriggerContext>): void;
|
||||
public abstract async execute(context: ActionContext): Promise<void>;
|
||||
}
|
||||
|
|
|
@ -48,9 +48,7 @@ describe('isCompatible()', () => {
|
|||
}),
|
||||
}),
|
||||
} as any,
|
||||
triggerContext: {
|
||||
filters: [],
|
||||
},
|
||||
filters: [],
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
@ -65,9 +63,7 @@ describe('isCompatible()', () => {
|
|||
}),
|
||||
}),
|
||||
} as any,
|
||||
triggerContext: {
|
||||
filters: [],
|
||||
},
|
||||
filters: [],
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
@ -83,25 +79,8 @@ describe('isCompatible()', () => {
|
|||
}),
|
||||
}),
|
||||
} as any,
|
||||
triggerContext: {
|
||||
// filters: [],
|
||||
} as any,
|
||||
});
|
||||
} as any);
|
||||
expect(result1).toBe(false);
|
||||
|
||||
const result2 = await action.isCompatible({
|
||||
embeddable: {
|
||||
getRoot: () => ({
|
||||
getInput: () => ({
|
||||
filters: [],
|
||||
}),
|
||||
}),
|
||||
} as any,
|
||||
// triggerContext: {
|
||||
// filters: [],
|
||||
// } as any
|
||||
});
|
||||
expect(result2).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -125,42 +104,41 @@ describe('execute()', () => {
|
|||
const error = expectError(() =>
|
||||
action.execute({
|
||||
embeddable: getEmbeddable(),
|
||||
triggerContext: {},
|
||||
} as any)
|
||||
);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
test('updates filter input on success', () => {
|
||||
test('updates filter input on success', async done => {
|
||||
const action = new ApplyFilterAction();
|
||||
const [embeddable, root] = getEmbeddable();
|
||||
|
||||
action.execute({
|
||||
await action.execute({
|
||||
embeddable,
|
||||
triggerContext: {
|
||||
filters: ['FILTER' as any],
|
||||
},
|
||||
filters: ['FILTER' as any],
|
||||
});
|
||||
|
||||
expect(root.updateInput).toHaveBeenCalledTimes(1);
|
||||
expect(root.updateInput.mock.calls[0][0]).toMatchObject({
|
||||
filters: ['FILTER'],
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('checks if action isCompatible', () => {
|
||||
test('checks if action isCompatible', async done => {
|
||||
const action = new ApplyFilterAction();
|
||||
const spy = jest.spyOn(action, 'isCompatible');
|
||||
const [embeddable] = getEmbeddable();
|
||||
|
||||
action.execute({
|
||||
await action.execute({
|
||||
embeddable,
|
||||
triggerContext: {
|
||||
filters: ['FILTER' as any],
|
||||
},
|
||||
filters: ['FILTER' as any],
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,14 +20,18 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { IEmbeddable, EmbeddableInput } from '../embeddables';
|
||||
import { Action, ActionContext } from './action';
|
||||
import { Action } from './action';
|
||||
import { IncompatibleActionError } from '../errors';
|
||||
|
||||
export const APPLY_FILTER_ACTION = 'APPLY_FILTER_ACTION';
|
||||
|
||||
type RootEmbeddable = IEmbeddable<EmbeddableInput & { filters: Filter[] }>;
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export class ApplyFilterAction extends Action<IEmbeddable, { filters: Filter[] }> {
|
||||
export class ApplyFilterAction extends Action<ActionContext> {
|
||||
public readonly type = APPLY_FILTER_ACTION;
|
||||
|
||||
constructor() {
|
||||
|
@ -40,30 +44,27 @@ export class ApplyFilterAction extends Action<IEmbeddable, { filters: Filter[] }
|
|||
});
|
||||
}
|
||||
|
||||
public async isCompatible(context: ActionContext<IEmbeddable, { filters: Filter[] }>) {
|
||||
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.triggerContext &&
|
||||
context.triggerContext.filters !== undefined
|
||||
);
|
||||
return Boolean(root.getInput().filters !== undefined && context.filters !== undefined);
|
||||
}
|
||||
|
||||
public execute({
|
||||
embeddable,
|
||||
triggerContext,
|
||||
}: ActionContext<IEmbeddable, { filters: Filter[] }>) {
|
||||
if (!triggerContext) {
|
||||
throw new Error('Applying a filter requires a filter as context');
|
||||
public async execute({ embeddable, filters }: ActionContext) {
|
||||
if (!filters || !embeddable) {
|
||||
throw new Error('Applying a filter requires a filter and embeddable as context');
|
||||
}
|
||||
const root = embeddable.getRoot() as RootEmbeddable;
|
||||
|
||||
if (!this.isCompatible({ triggerContext, embeddable })) {
|
||||
if (!(await this.isCompatible({ embeddable, filters }))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
||||
const root = embeddable.getRoot() as RootEmbeddable;
|
||||
|
||||
root.updateInput({
|
||||
filters: triggerContext.filters,
|
||||
filters,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,18 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Action, ActionContext } from './action';
|
||||
import { Action } from './action';
|
||||
import { GetEmbeddableFactory, ViewMode } from '../types';
|
||||
import { EmbeddableFactoryNotFoundError } from '../errors';
|
||||
import { IEmbeddable } from '../embeddables';
|
||||
|
||||
export const EDIT_PANEL_ACTION_ID = 'editPanel';
|
||||
|
||||
export class EditPanelAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class EditPanelAction extends Action<ActionContext> {
|
||||
public readonly type = EDIT_PANEL_ACTION_ID;
|
||||
constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) {
|
||||
super(EDIT_PANEL_ACTION_ID);
|
||||
|
@ -56,8 +61,8 @@ export class EditPanelAction extends Action {
|
|||
return Boolean(canEditEmbeddable && inDashboardEditMode);
|
||||
}
|
||||
|
||||
public execute() {
|
||||
return undefined;
|
||||
public async execute() {
|
||||
return;
|
||||
}
|
||||
|
||||
public getHref({ embeddable }: ActionContext): string {
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { Action, ActionContext } from './action';
|
||||
export { Action } from './action';
|
||||
export * from './apply_filter_action';
|
||||
export * from './edit_panel_action';
|
||||
|
|
|
@ -20,21 +20,21 @@
|
|||
import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Action, ActionContext } from '../actions';
|
||||
import { Action } from '../actions';
|
||||
|
||||
/**
|
||||
* Transforms an array of Actions to the shape EuiContextMenuPanel expects.
|
||||
*/
|
||||
export async function buildContextMenuForActions({
|
||||
export async function buildContextMenuForActions<A>({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
}: {
|
||||
actions: Action[];
|
||||
actionContext: ActionContext;
|
||||
actions: Array<Action<A>>;
|
||||
actionContext: A;
|
||||
closeMenu: () => void;
|
||||
}): Promise<EuiContextMenuPanelDescriptor> {
|
||||
const menuItems = await buildEuiContextMenuPanelItems({
|
||||
const menuItems = await buildEuiContextMenuPanelItems<A>({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
|
@ -52,13 +52,13 @@ export async function buildContextMenuForActions({
|
|||
/**
|
||||
* Transform an array of Actions into the shape needed to build an EUIContextMenu
|
||||
*/
|
||||
async function buildEuiContextMenuPanelItems({
|
||||
async function buildEuiContextMenuPanelItems<A>({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
}: {
|
||||
actions: Action[];
|
||||
actionContext: ActionContext;
|
||||
actions: Array<Action<A>>;
|
||||
actionContext: A;
|
||||
closeMenu: () => void;
|
||||
}) {
|
||||
const items: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
|
@ -88,13 +88,13 @@ async function buildEuiContextMenuPanelItems({
|
|||
* @param {Embeddable} embeddable
|
||||
* @return {EuiContextMenuPanelItemDescriptor}
|
||||
*/
|
||||
function convertPanelActionToContextMenuItem({
|
||||
function convertPanelActionToContextMenuItem<A>({
|
||||
action,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
}: {
|
||||
action: Action;
|
||||
actionContext: ActionContext;
|
||||
action: Action<A>;
|
||||
actionContext: A;
|
||||
closeMenu: () => void;
|
||||
}): EuiContextMenuPanelItemDescriptor {
|
||||
const menuPanelItem: EuiContextMenuPanelItemDescriptor = {
|
||||
|
|
|
@ -37,7 +37,7 @@ import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel
|
|||
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, ActionContext } from '../actions';
|
||||
import { EditPanelAction, Action } from '../actions';
|
||||
import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal';
|
||||
import { Start as InspectorStartContract } from '../../../../../../../../plugins/inspector/public';
|
||||
|
||||
|
@ -192,7 +192,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
});
|
||||
|
||||
const createGetUserData = (overlays: CoreStart['overlays']) =>
|
||||
async function getUserData(context: ActionContext) {
|
||||
async function getUserData(context: { embeddable: IEmbeddable }) {
|
||||
return new Promise<{ title: string | undefined }>(resolve => {
|
||||
const session = overlays.openModal(
|
||||
<CustomizePanelModal
|
||||
|
|
|
@ -89,9 +89,8 @@ test('Is not compatible when container is in view mode', async () => {
|
|||
|
||||
test('Is not compatible when embeddable is not a container', async () => {
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable,
|
||||
})
|
||||
// @ts-ignore
|
||||
await action.isCompatible({ embeddable })
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
@ -18,14 +18,19 @@
|
|||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types';
|
||||
import { Action, ActionContext } from '../../../../actions';
|
||||
import { Action } from '../../../../actions';
|
||||
import { openAddPanelFlyout } from './open_add_panel_flyout';
|
||||
import { NotificationsStart } from '../../../../../../../../../../../core/public';
|
||||
import { KibanaReactOverlays } from '../../../../../../../../../../../plugins/kibana_react/public';
|
||||
import { IContainer } from '../../../../containers';
|
||||
|
||||
export const ADD_PANEL_ACTION_ID = 'ADD_PANEL_ACTION_ID';
|
||||
|
||||
export class AddPanelAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IContainer;
|
||||
}
|
||||
|
||||
export class AddPanelAction extends Action<ActionContext> {
|
||||
public readonly type = ADD_PANEL_ACTION_ID;
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -18,14 +18,19 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Action, ActionContext } from '../../../../actions';
|
||||
import { Action } from '../../../../actions';
|
||||
import { ViewMode } from '../../../../types';
|
||||
import { IEmbeddable } from '../../../../embeddables';
|
||||
|
||||
const CUSTOMIZE_PANEL_ACTION_ID = 'CUSTOMIZE_PANEL_ACTION_ID';
|
||||
|
||||
type GetUserData = (context: ActionContext) => Promise<{ title: string | undefined }>;
|
||||
|
||||
export class CustomizePanelTitleAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class CustomizePanelTitleAction extends Action<ActionContext> {
|
||||
public readonly type = CUSTOMIZE_PANEL_ACTION_ID;
|
||||
|
||||
constructor(private readonly getDataFromUser: GetUserData) {
|
||||
|
|
|
@ -18,12 +18,17 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Action, ActionContext } from '../../../actions';
|
||||
import { Action } from '../../../actions';
|
||||
import { Start as InspectorStartContract } from '../../../../../../../../../../plugins/inspector/public';
|
||||
import { IEmbeddable } from '../../../embeddables';
|
||||
|
||||
export const INSPECT_PANEL_ACTION_ID = 'openInspector';
|
||||
|
||||
export class InspectPanelAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class InspectPanelAction extends Action<ActionContext> {
|
||||
public readonly type = INSPECT_PANEL_ACTION_ID;
|
||||
|
||||
constructor(private readonly inspector: InspectorStartContract) {
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
} from '../../../test_samples/embeddables/filterable_embeddable';
|
||||
import { FilterableEmbeddableFactory } from '../../../test_samples/embeddables/filterable_embeddable_factory';
|
||||
import { FilterableContainer } from '../../../test_samples/embeddables/filterable_container';
|
||||
import { GetEmbeddableFactory } from '../../../types';
|
||||
import { GetEmbeddableFactory, ViewMode } from '../../../types';
|
||||
import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable';
|
||||
|
||||
const embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
|
@ -45,7 +45,7 @@ beforeEach(async () => {
|
|||
query: { match: {} },
|
||||
};
|
||||
container = new FilterableContainer(
|
||||
{ id: 'hello', panels: {}, filters: [derivedFilter] },
|
||||
{ id: 'hello', panels: {}, filters: [derivedFilter], viewMode: ViewMode.EDIT },
|
||||
getFactory
|
||||
);
|
||||
|
||||
|
@ -55,6 +55,7 @@ beforeEach(async () => {
|
|||
FilterableEmbeddable
|
||||
>(FILTERABLE_EMBEDDABLE, {
|
||||
id: '123',
|
||||
viewMode: ViewMode.EDIT,
|
||||
});
|
||||
|
||||
if (isErrorEmbeddable(filterableEmbeddable)) {
|
||||
|
@ -68,7 +69,7 @@ test('Removes the embeddable', async () => {
|
|||
const removePanelAction = new RemovePanelAction();
|
||||
expect(container.getChild(embeddable.id)).toBeDefined();
|
||||
|
||||
removePanelAction.execute({ embeddable });
|
||||
await removePanelAction.execute({ embeddable });
|
||||
|
||||
expect(container.getChild(embeddable.id)).toBeUndefined();
|
||||
});
|
||||
|
|
|
@ -19,8 +19,9 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ContainerInput, IContainer } from '../../../containers';
|
||||
import { ViewMode } from '../../../types';
|
||||
import { Action, ActionContext } from '../../../actions';
|
||||
import { Action } from '../../../actions';
|
||||
import { IncompatibleActionError } from '../../../errors';
|
||||
import { IEmbeddable } from '../../../embeddables';
|
||||
|
||||
export const REMOVE_PANEL_ACTION = 'deletePanel';
|
||||
|
||||
|
@ -28,13 +29,17 @@ interface ExpandedPanelInput extends ContainerInput {
|
|||
expandedPanelId: string;
|
||||
}
|
||||
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
function hasExpandedPanelInput(
|
||||
container: IContainer
|
||||
): container is IContainer<{}, ExpandedPanelInput> {
|
||||
return (container as IContainer<{}, ExpandedPanelInput>).getInput().expandedPanelId !== undefined;
|
||||
}
|
||||
|
||||
export class RemovePanelAction extends Action {
|
||||
export class RemovePanelAction extends Action<ActionContext> {
|
||||
public readonly type = REMOVE_PANEL_ACTION;
|
||||
constructor() {
|
||||
super(REMOVE_PANEL_ACTION);
|
||||
|
@ -63,8 +68,8 @@ export class RemovePanelAction extends Action {
|
|||
);
|
||||
}
|
||||
|
||||
public execute({ embeddable }: ActionContext) {
|
||||
if (!embeddable.parent || !this.isCompatible({ embeddable })) {
|
||||
public async execute({ embeddable }: ActionContext) {
|
||||
if (!embeddable.parent || !(await this.isCompatible({ embeddable }))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
embeddable.parent.removeEmbeddable(embeddable.id);
|
||||
|
|
|
@ -18,11 +18,15 @@
|
|||
*/
|
||||
|
||||
import { ViewMode } from '../../types';
|
||||
import { Action, ActionContext } from '../../actions';
|
||||
import { Action } from '../../actions';
|
||||
import { IEmbeddable } from '../../embeddables';
|
||||
|
||||
export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION';
|
||||
|
||||
export class EditModeAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
export class EditModeAction extends Action<ActionContext> {
|
||||
public readonly type = EDIT_MODE_ACTION;
|
||||
|
||||
constructor() {
|
||||
|
@ -37,7 +41,7 @@ export class EditModeAction extends Action {
|
|||
return context.embeddable.getInput().viewMode === ViewMode.EDIT;
|
||||
}
|
||||
|
||||
execute() {
|
||||
async execute() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export class HelloWorldAction extends Action {
|
|||
return 'Hello World Action!';
|
||||
}
|
||||
|
||||
public execute() {
|
||||
public async execute() {
|
||||
const flyoutSession = this.overlays.openFlyout(
|
||||
<EuiFlyout ownFocus onClose={() => flyoutSession && flyoutSession.close()}>
|
||||
Hello World, I am a hello world action!
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Action, ActionContext } from '../../actions';
|
||||
import { Action } from '../../actions';
|
||||
|
||||
export const RESTRICTED_ACTION = 'RESTRICTED_ACTION';
|
||||
|
||||
export class RestrictedAction extends Action {
|
||||
export class RestrictedAction<A> extends Action<A> {
|
||||
public readonly type = RESTRICTED_ACTION;
|
||||
|
||||
private isCompatibleFn: (context: ActionContext) => boolean;
|
||||
constructor(isCompatible: (context: ActionContext) => boolean) {
|
||||
private isCompatibleFn: (context: A) => boolean;
|
||||
constructor(isCompatible: (context: A) => boolean) {
|
||||
super(RESTRICTED_ACTION);
|
||||
this.isCompatibleFn = isCompatible;
|
||||
}
|
||||
|
@ -34,9 +34,9 @@ export class RestrictedAction extends Action {
|
|||
return `I am only sometimes compatible`;
|
||||
}
|
||||
|
||||
async isCompatible(context: ActionContext) {
|
||||
async isCompatible(context: A) {
|
||||
return this.isCompatibleFn(context);
|
||||
}
|
||||
|
||||
execute() {}
|
||||
async execute() {}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Action, ActionContext } from '../../actions';
|
||||
import { Action } from '../../actions';
|
||||
import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables';
|
||||
import { IncompatibleActionError } from '../../errors';
|
||||
|
||||
|
@ -36,7 +36,12 @@ export function hasFullNameOutput(
|
|||
);
|
||||
}
|
||||
|
||||
export class SayHelloAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: Embeddable<EmbeddableInput, FullNameEmbeddableOutput>;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class SayHelloAction extends Action<ActionContext> {
|
||||
public readonly type = SAY_HELLO_ACTION;
|
||||
private sayHello: (name: string) => void;
|
||||
|
||||
|
@ -53,9 +58,7 @@ export class SayHelloAction extends Action {
|
|||
|
||||
// Can use typescript generics to get compiler time warnings for immediate feedback if
|
||||
// the context is not compatible.
|
||||
async isCompatible(
|
||||
context: ActionContext<Embeddable<EmbeddableInput, FullNameEmbeddableOutput>>
|
||||
) {
|
||||
async isCompatible(context: ActionContext) {
|
||||
// Option 1: only compatible with Greeting Embeddables.
|
||||
// return context.embeddable.type === CONTACT_CARD_EMBEDDABLE;
|
||||
|
||||
|
@ -63,20 +66,15 @@ export class SayHelloAction extends Action {
|
|||
return hasFullNameOutput(context.embeddable);
|
||||
}
|
||||
|
||||
async execute(
|
||||
context: ActionContext<
|
||||
Embeddable<EmbeddableInput, FullNameEmbeddableOutput>,
|
||||
{ message?: string }
|
||||
>
|
||||
) {
|
||||
async execute(context: ActionContext) {
|
||||
if (!(await this.isCompatible(context))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
||||
const greeting = `Hello, ${context.embeddable.getOutput().fullName}`;
|
||||
|
||||
if (context.triggerContext && context.triggerContext.message) {
|
||||
this.sayHello(`${greeting}. ${context.triggerContext.message}`);
|
||||
if (context.message) {
|
||||
this.sayHello(`${greeting}. ${context.message}`);
|
||||
} else {
|
||||
this.sayHello(greeting);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlyoutBody } from '@elastic/eui';
|
||||
import { Action, ActionContext, IncompatibleActionError } from '../..';
|
||||
import { Action, IncompatibleActionError } from '../..';
|
||||
import { Embeddable, EmbeddableInput } from '../../embeddables';
|
||||
import { GetMessageModal } from './get_message_modal';
|
||||
import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action';
|
||||
|
@ -26,6 +26,10 @@ import { CoreStart } from '../../../../../../../../../core/public';
|
|||
|
||||
export const SEND_MESSAGE_ACTION = 'SEND_MESSAGE_ACTION';
|
||||
|
||||
interface ActionContext {
|
||||
embeddable: Embeddable<EmbeddableInput, FullNameEmbeddableOutput>;
|
||||
message: string;
|
||||
}
|
||||
export class SendMessageAction extends Action {
|
||||
public readonly type = SEND_MESSAGE_ACTION;
|
||||
|
||||
|
@ -37,28 +41,18 @@ export class SendMessageAction extends Action {
|
|||
return 'Send message';
|
||||
}
|
||||
|
||||
async isCompatible(
|
||||
context: ActionContext<Embeddable<EmbeddableInput, FullNameEmbeddableOutput>>
|
||||
) {
|
||||
async isCompatible(context: ActionContext) {
|
||||
return hasFullNameOutput(context.embeddable);
|
||||
}
|
||||
|
||||
async sendMessage(
|
||||
context: ActionContext<Embeddable<EmbeddableInput, FullNameEmbeddableOutput>>,
|
||||
message: string
|
||||
) {
|
||||
async sendMessage(context: ActionContext, message: string) {
|
||||
const greeting = `Hello, ${context.embeddable.getOutput().fullName}`;
|
||||
|
||||
const content = message ? `${greeting}. ${message}` : greeting;
|
||||
this.overlays.openFlyout(<EuiFlyoutBody>{content}</EuiFlyoutBody>);
|
||||
}
|
||||
|
||||
async execute(
|
||||
context: ActionContext<
|
||||
Embeddable<EmbeddableInput, FullNameEmbeddableOutput>,
|
||||
{ message?: string }
|
||||
>
|
||||
) {
|
||||
async execute(context: ActionContext) {
|
||||
if (!(await this.isCompatible(context))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Action, ActionContext } from './actions';
|
||||
import { IEmbeddable } from './embeddables';
|
||||
import { IContainer } from './containers';
|
||||
import { Action } from './actions';
|
||||
import { EmbeddableFactory } from './embeddables/embeddable_factory';
|
||||
import { Adapters } from '../../../../../../../plugins/inspector/public';
|
||||
|
||||
|
@ -55,18 +53,10 @@ export interface SavedObjectMetaData<T> {
|
|||
showSavedObject?(savedObject: any): any;
|
||||
}
|
||||
|
||||
export interface TriggerContext {
|
||||
embeddable: IEmbeddable;
|
||||
container?: IContainer;
|
||||
}
|
||||
|
||||
export type ExecuteTriggerActions = (
|
||||
export type ExecuteTriggerActions = <A>(triggerId: string, actionContext: A) => Promise<void>;
|
||||
export type GetActionsCompatibleWithTrigger = <C>(
|
||||
triggerId: string,
|
||||
actionContext: ActionContext
|
||||
) => Promise<void>;
|
||||
export type GetActionsCompatibleWithTrigger = (
|
||||
triggerId: string,
|
||||
context: TriggerContext
|
||||
context: C
|
||||
) => Promise<Action[]>;
|
||||
export type GetEmbeddableFactory = (id: string) => EmbeddableFactory | undefined;
|
||||
export type GetEmbeddableFactories = () => IterableIterator<EmbeddableFactory>;
|
||||
|
|
|
@ -85,7 +85,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
|
|||
query: { match: { extension: { query: 'foo' } } },
|
||||
};
|
||||
|
||||
applyFilterAction.execute({ embeddable, triggerContext: { filters: [filter] } });
|
||||
await applyFilterAction.execute({ embeddable, filters: [filter] });
|
||||
expect(root.getInput().filters.length).toBe(1);
|
||||
expect(node1.getInput().filters.length).toBe(1);
|
||||
expect(embeddable.getInput().filters.length).toBe(1);
|
||||
|
@ -124,6 +124,7 @@ test('ApplyFilterAction is incompatible if the root container does not accept a
|
|||
throw new Error();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
expect(await applyFilterAction.isCompatible({ embeddable })).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -160,6 +161,7 @@ test('trying to execute on incompatible context throws an error ', async () => {
|
|||
}
|
||||
|
||||
async function check() {
|
||||
// @ts-ignore
|
||||
await applyFilterAction.execute({ embeddable });
|
||||
}
|
||||
await expect(check()).rejects.toThrow(Error);
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { testPlugin, TestPluginReturn } from './test_plugin';
|
||||
import { of } from './helpers';
|
||||
import { ActionContext, Action, openContextMenu } from '../lib';
|
||||
import { Action, openContextMenu, IEmbeddable } from '../lib';
|
||||
import {
|
||||
ContactCardEmbeddable,
|
||||
CONTACT_USER_TRIGGER,
|
||||
|
@ -31,11 +31,11 @@ jest.mock('../lib/context_menu_actions');
|
|||
const executeFn = jest.fn();
|
||||
const openContextMenuSpy = (openContextMenu as any) as jest.SpyInstance;
|
||||
|
||||
class TestAction extends Action {
|
||||
class TestAction<A> extends Action<A> {
|
||||
public readonly type = 'testAction';
|
||||
public checkCompatibility: (context: ActionContext) => boolean;
|
||||
public checkCompatibility: (context: A) => boolean;
|
||||
|
||||
constructor(id: string, checkCompatibility: (context: ActionContext) => boolean) {
|
||||
constructor(id: string, checkCompatibility: (context: A) => boolean) {
|
||||
super(id);
|
||||
this.checkCompatibility = checkCompatibility;
|
||||
}
|
||||
|
@ -44,11 +44,11 @@ class TestAction extends Action {
|
|||
return 'test';
|
||||
}
|
||||
|
||||
async isCompatible(context: ActionContext) {
|
||||
async isCompatible(context: A) {
|
||||
return this.checkCompatibility(context);
|
||||
}
|
||||
|
||||
execute(context: ActionContext) {
|
||||
async execute(context: unknown) {
|
||||
executeFn(context);
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +124,10 @@ test('does not execute an incompatible action', async () => {
|
|||
title: 'My trigger',
|
||||
actionIds: ['test1'],
|
||||
};
|
||||
const action = new TestAction('test1', ({ embeddable }) => embeddable.id === 'executeme');
|
||||
const action = new TestAction<{ embeddable: IEmbeddable }>(
|
||||
'test1',
|
||||
({ embeddable }) => embeddable.id === 'executeme'
|
||||
);
|
||||
const embeddable = new ContactCardEmbeddable(
|
||||
{
|
||||
id: 'executeme',
|
||||
|
@ -187,7 +190,7 @@ test('passes whole action context to isCompatible()', async () => {
|
|||
title: 'My trigger',
|
||||
actionIds: ['test'],
|
||||
};
|
||||
const action = new TestAction('test', ({ triggerContext }) => {
|
||||
const action = new TestAction<{ triggerContext: any }>('test', ({ triggerContext }) => {
|
||||
expect(triggerContext).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ 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 { ActionContext, CONTEXT_MENU_TRIGGER } from '../lib';
|
||||
import { CONTEXT_MENU_TRIGGER, IEmbeddable } from '../lib';
|
||||
import { of } from './helpers';
|
||||
|
||||
let action: SayHelloAction;
|
||||
|
@ -73,7 +73,7 @@ test('getTriggerCompatibleActions returns attached actions', async () => {
|
|||
|
||||
test('filters out actions not applicable based on the context', async () => {
|
||||
const { setup, doStart } = embeddables;
|
||||
const restrictedAction = new RestrictedAction((context: ActionContext) => {
|
||||
const restrictedAction = new RestrictedAction<{ embeddable: IEmbeddable }>(context => {
|
||||
return context.embeddable.id === 'accept';
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ if (timefield) {
|
|||
class="fa fa-search-plus kbnDocTableRowFilterButton"
|
||||
data-column="<%- column %>"
|
||||
tooltip-append-to-body="1"
|
||||
data-test-subj="docTableCellFilter"
|
||||
tooltip="{{ ::'kbn.docTable.tableRow.filterForValueButtonTooltip' | i18n: {defaultMessage: 'Filter for value'} }}"
|
||||
aria-label="{{ ::'kbn.docTable.tableRow.filterForValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter for value'} }}"
|
||||
></button>
|
||||
|
@ -25,6 +26,7 @@ if (timefield) {
|
|||
ng-click="inlineFilter($event, '-')"
|
||||
class="fa fa-search-minus kbnDocTableRowFilterButton"
|
||||
data-column="<%- column %>"
|
||||
data-test-subj="docTableCellFilterNegate"
|
||||
tooltip="{{ ::'kbn.docTable.tableRow.filterOutValueButtonTooltip' | i18n: {defaultMessage: 'Filter out value'} }}"
|
||||
aria-label="{{ ::'kbn.docTable.tableRow.filterOutValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter out value'} }}"
|
||||
tooltip-append-to-body="1"
|
||||
|
|
|
@ -257,9 +257,7 @@ export class SearchEmbeddable extends Embeddable<SearchInput, SearchOutput>
|
|||
|
||||
await this.executeTriggerActions(APPLY_FILTER_TRIGGER, {
|
||||
embeddable: this,
|
||||
triggerContext: {
|
||||
filters,
|
||||
},
|
||||
filters,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -133,10 +133,10 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.dashboard.setTimepickerInDataRange();
|
||||
});
|
||||
|
||||
it('are added when pie chart legend item is clicked', async function () {
|
||||
await dashboardAddPanel.addVisualization('Rendering Test: pie');
|
||||
it('are added when a cell magnifying glass is clicked', async function () {
|
||||
await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search');
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await pieChart.filterByLegendItem('4,886');
|
||||
await testSubjects.click('docTableCellFilter');
|
||||
|
||||
const filterCount = await filterBar.getFilterCount();
|
||||
expect(filterCount).to.equal(1);
|
||||
|
|
|
@ -22,12 +22,16 @@ import { npStart } from 'ui/new_platform';
|
|||
|
||||
import {
|
||||
Action,
|
||||
ActionContext,
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
IEmbeddable,
|
||||
} 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';
|
||||
|
||||
class SamplePanelAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
class SamplePanelAction extends Action<ActionContext> {
|
||||
public readonly type = 'samplePanelAction';
|
||||
|
||||
constructor() {
|
||||
|
@ -38,7 +42,7 @@ class SamplePanelAction extends Action {
|
|||
return 'Sample Panel Action';
|
||||
}
|
||||
|
||||
public execute = ({ embeddable }: ActionContext) => {
|
||||
public execute = async ({ embeddable }: ActionContext) => {
|
||||
if (!embeddable) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ class SamplePanelLink extends Action {
|
|||
return 'Sample panel Link';
|
||||
}
|
||||
|
||||
public execute() {
|
||||
public async execute() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -339,6 +339,7 @@ test('Attempting to execute on incompatible embeddable throws an error', async (
|
|||
});
|
||||
|
||||
async function check() {
|
||||
// @ts-ignore
|
||||
await action.execute({ embeddable: child });
|
||||
}
|
||||
await expect(check()).rejects.toThrow(Error);
|
||||
|
|
|
@ -14,7 +14,6 @@ import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../../src/legacy/core_
|
|||
import {
|
||||
Action,
|
||||
IEmbeddable,
|
||||
ActionContext,
|
||||
IncompatibleActionError,
|
||||
Embeddable,
|
||||
EmbeddableInput,
|
||||
|
@ -41,7 +40,11 @@ function isVisualizeEmbeddable(
|
|||
return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE;
|
||||
}
|
||||
|
||||
export class CustomTimeRangeAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: Embeddable<TimeRangeInput>;
|
||||
}
|
||||
|
||||
export class CustomTimeRangeAction extends Action<ActionContext> {
|
||||
public readonly type = CUSTOM_TIME_RANGE;
|
||||
private openModal: OpenModal;
|
||||
private dateFormat?: string;
|
||||
|
@ -76,10 +79,11 @@ export class CustomTimeRangeAction extends Action {
|
|||
public async isCompatible({ embeddable }: ActionContext) {
|
||||
const isInputControl =
|
||||
isVisualizeEmbeddable(embeddable) &&
|
||||
embeddable.getOutput().visTypeName === 'input_control_vis';
|
||||
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'input_control_vis';
|
||||
|
||||
const isMarkdown =
|
||||
isVisualizeEmbeddable(embeddable) && embeddable.getOutput().visTypeName === 'markdown';
|
||||
isVisualizeEmbeddable(embeddable) &&
|
||||
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown';
|
||||
return Boolean(
|
||||
embeddable &&
|
||||
hasTimeRange(embeddable) &&
|
||||
|
|
|
@ -11,7 +11,6 @@ import { TimeRange } from '../../../../../../../src/plugins/data/public';
|
|||
import {
|
||||
Action,
|
||||
IEmbeddable,
|
||||
ActionContext,
|
||||
IncompatibleActionError,
|
||||
Embeddable,
|
||||
EmbeddableInput,
|
||||
|
@ -33,7 +32,11 @@ function hasTimeRange(
|
|||
return (embeddable as Embeddable<TimeRangeInput>).getInput().timeRange !== undefined;
|
||||
}
|
||||
|
||||
export class CustomTimeRangeBadge extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: Embeddable<TimeRangeInput>;
|
||||
}
|
||||
|
||||
export class CustomTimeRangeBadge extends Action<ActionContext> {
|
||||
public readonly type = CUSTOM_TIME_RANGE_BADGE;
|
||||
private openModal: OpenModal;
|
||||
private dateFormat: string;
|
||||
|
@ -55,7 +58,7 @@ export class CustomTimeRangeBadge extends Action {
|
|||
this.commonlyUsedRanges = commonlyUsedRanges;
|
||||
}
|
||||
|
||||
public getDisplayName({ embeddable }: ActionContext<Embeddable<TimeRangeInput>>) {
|
||||
public getDisplayName({ embeddable }: ActionContext) {
|
||||
return prettyDuration(
|
||||
embeddable.getInput().timeRange.from,
|
||||
embeddable.getInput().timeRange.to,
|
||||
|
|
|
@ -15,7 +15,6 @@ import { EuiIcon } from '@elastic/eui';
|
|||
|
||||
import {
|
||||
Action,
|
||||
ActionContext,
|
||||
ViewMode,
|
||||
IncompatibleActionError,
|
||||
IEmbeddable,
|
||||
|
@ -37,7 +36,12 @@ function isSavedSearchEmbeddable(
|
|||
): embeddable is ISearchEmbeddable {
|
||||
return embeddable.type === SEARCH_EMBEDDABLE_TYPE;
|
||||
}
|
||||
class GetCsvReportPanelAction extends Action<ISearchEmbeddable> {
|
||||
|
||||
interface ActionContext {
|
||||
embeddable: ISearchEmbeddable;
|
||||
}
|
||||
|
||||
class GetCsvReportPanelAction extends Action<ActionContext> {
|
||||
private isDownloading: boolean;
|
||||
public readonly type = CSV_REPORTING_ACTION;
|
||||
|
||||
|
@ -82,7 +86,7 @@ class GetCsvReportPanelAction extends Action<ISearchEmbeddable> {
|
|||
return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search';
|
||||
};
|
||||
|
||||
public execute = async (context: ActionContext<ISearchEmbeddable>) => {
|
||||
public execute = async (context: ActionContext) => {
|
||||
const { embeddable } = context;
|
||||
|
||||
if (!isSavedSearchEmbeddable(embeddable)) {
|
||||
|
|
|
@ -29,10 +29,6 @@ const isEmbeddable = (
|
|||
return get('type', embeddable) != null;
|
||||
};
|
||||
|
||||
const isTriggerContext = (triggerContext: unknown): triggerContext is { filters: Filter[] } => {
|
||||
return typeof triggerContext === 'object';
|
||||
};
|
||||
|
||||
describe('ApplySiemFilterAction', () => {
|
||||
let applyFilterQueryFromKueryExpression: (expression: string) => void;
|
||||
|
||||
|
@ -57,7 +53,7 @@ describe('ApplySiemFilterAction', () => {
|
|||
});
|
||||
|
||||
describe('#isCompatible', () => {
|
||||
test('when embeddable type is MAP_SAVED_OBJECT_TYPE and triggerContext filters exist, returns true', async () => {
|
||||
test('when embeddable type is MAP_SAVED_OBJECT_TYPE and filters exist, returns true', async () => {
|
||||
const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression });
|
||||
const embeddable = {
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
|
@ -65,9 +61,7 @@ describe('ApplySiemFilterAction', () => {
|
|||
if (isEmbeddable(embeddable)) {
|
||||
const result = await action.isCompatible({
|
||||
embeddable,
|
||||
triggerContext: {
|
||||
filters: [],
|
||||
},
|
||||
filters: [],
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
} else {
|
||||
|
@ -75,7 +69,7 @@ describe('ApplySiemFilterAction', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('when embeddable type is MAP_SAVED_OBJECT_TYPE and triggerContext does not exist, returns false', async () => {
|
||||
test('when embeddable type is MAP_SAVED_OBJECT_TYPE and filters do not exist, returns false', async () => {
|
||||
const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression });
|
||||
const embeddable = {
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
|
@ -83,30 +77,14 @@ describe('ApplySiemFilterAction', () => {
|
|||
if (isEmbeddable(embeddable)) {
|
||||
const result = await action.isCompatible({
|
||||
embeddable,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
expect(result).toBe(false);
|
||||
} else {
|
||||
throw new Error('Invalid embeddable in unit test');
|
||||
}
|
||||
});
|
||||
|
||||
test('when embeddable type is MAP_SAVED_OBJECT_TYPE and triggerContext filters do not exist, returns false', async () => {
|
||||
const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression });
|
||||
const embeddable = {
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
};
|
||||
const triggerContext = {};
|
||||
if (isEmbeddable(embeddable) && isTriggerContext(triggerContext)) {
|
||||
const result = await action.isCompatible({
|
||||
embeddable,
|
||||
triggerContext,
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
} else {
|
||||
throw new Error('Invalid embeddable/triggerContext in unit test');
|
||||
}
|
||||
});
|
||||
|
||||
test('when embeddable type is not MAP_SAVED_OBJECT_TYPE, returns false', async () => {
|
||||
const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression });
|
||||
const embeddable = {
|
||||
|
@ -115,9 +93,7 @@ describe('ApplySiemFilterAction', () => {
|
|||
if (isEmbeddable(embeddable)) {
|
||||
const result = await action.isCompatible({
|
||||
embeddable,
|
||||
triggerContext: {
|
||||
filters: [],
|
||||
},
|
||||
filters: [],
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
} else {
|
||||
|
@ -136,7 +112,8 @@ describe('ApplySiemFilterAction', () => {
|
|||
const error = expectError(() =>
|
||||
action.execute({
|
||||
embeddable,
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any)
|
||||
);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
} else {
|
||||
|
@ -152,24 +129,27 @@ describe('ApplySiemFilterAction', () => {
|
|||
query: { query: '' },
|
||||
}),
|
||||
};
|
||||
const triggerContext = {
|
||||
filters: [
|
||||
{
|
||||
query: {
|
||||
match: {
|
||||
'host.name': {
|
||||
query: 'zeek-newyork-sha-aa8df15',
|
||||
type: 'phrase',
|
||||
},
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
query: {
|
||||
match: {
|
||||
'host.name': {
|
||||
query: 'zeek-newyork-sha-aa8df15',
|
||||
type: 'phrase',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
if (isEmbeddable(embeddable) && isTriggerContext(triggerContext)) {
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
if (isEmbeddable(embeddable)) {
|
||||
await action.execute({
|
||||
embeddable,
|
||||
triggerContext,
|
||||
filters,
|
||||
});
|
||||
|
||||
expect(
|
||||
|
@ -177,7 +157,7 @@ describe('ApplySiemFilterAction', () => {
|
|||
.calls[0][0]
|
||||
).toBe('host.name: "zeek-newyork-sha-aa8df15"');
|
||||
} else {
|
||||
throw new Error('Invalid embeddable/triggerContext in unit test');
|
||||
throw new Error('Invalid embeddable in unit test');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,15 +9,17 @@ import { getOr } from 'lodash/fp';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore Missing type defs as maps moves to Typescript
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/common/constants';
|
||||
import {
|
||||
Action,
|
||||
ActionContext,
|
||||
} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions';
|
||||
import { Action } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions';
|
||||
import { IEmbeddable } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/embeddables';
|
||||
|
||||
export const APPLY_SIEM_FILTER_ACTION_ID = 'APPLY_SIEM_FILTER_ACTION_ID';
|
||||
|
||||
export class ApplySiemFilterAction extends Action {
|
||||
interface ActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export class ApplySiemFilterAction extends Action<ActionContext> {
|
||||
public readonly type = APPLY_SIEM_FILTER_ACTION_ID;
|
||||
private readonly applyFilterQueryFromKueryExpression: (expression: string) => void;
|
||||
|
||||
|
@ -36,26 +38,17 @@ export class ApplySiemFilterAction extends Action {
|
|||
});
|
||||
}
|
||||
|
||||
public async isCompatible(
|
||||
context: ActionContext<IEmbeddable, { filters: Filter[] }>
|
||||
): Promise<boolean> {
|
||||
return (
|
||||
context.embeddable.type === MAP_SAVED_OBJECT_TYPE &&
|
||||
context.triggerContext != null &&
|
||||
context.triggerContext.filters !== undefined
|
||||
);
|
||||
public async isCompatible(context: ActionContext): Promise<boolean> {
|
||||
return context.embeddable.type === MAP_SAVED_OBJECT_TYPE && context.filters !== undefined;
|
||||
}
|
||||
|
||||
public execute({
|
||||
embeddable,
|
||||
triggerContext,
|
||||
}: ActionContext<IEmbeddable, { filters: Filter[] }>) {
|
||||
if (!triggerContext) {
|
||||
public async execute({ embeddable, filters }: ActionContext) {
|
||||
if (!filters) {
|
||||
throw new Error('Applying a filter requires a filter as context');
|
||||
}
|
||||
|
||||
// Parse queryExpression from queryDSL and apply to SIEM global KQL Bar via redux
|
||||
const filterObject = getOr(null, 'filters[0].query.match', triggerContext);
|
||||
const filterObject = getOr(null, '[0].query.match', filters);
|
||||
|
||||
if (filterObject != null) {
|
||||
const filterQuery = getOr('', 'query.query', embeddable.getInput());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue