Skip to content

Commit

Permalink
POC
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant committed Jul 1, 2020
1 parent 1e16d70 commit 4b4f9ff
Show file tree
Hide file tree
Showing 40 changed files with 787 additions and 502 deletions.
16 changes: 8 additions & 8 deletions src/plugins/data/public/actions/apply_filter_action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@

import { i18n } from '@kbn/i18n';
import { toMountPoint } from '../../../kibana_react/public';
import { ActionByType, createAction, IncompatibleActionError } from '../../../ui_actions/public';
import {
ActionByType,
ApplyFilterTriggerContext,
createAction,
IncompatibleActionError,
} from '../../../ui_actions/public';
import { getOverlays, getIndexPatterns } from '../services';
import { applyFiltersPopover } from '../ui/apply_filters';
import { Filter, FilterManager, TimefilterContract, esFilters } from '..';

export const ACTION_GLOBAL_APPLY_FILTER = 'ACTION_GLOBAL_APPLY_FILTER';

export interface ApplyGlobalFilterActionContext {
filters: Filter[];
timeFieldName?: string;
}

async function isCompatible(context: ApplyGlobalFilterActionContext) {
async function isCompatible(context: ApplyFilterTriggerContext) {
return context.filters !== undefined;
}

Expand All @@ -49,7 +49,7 @@ export function createFilterAction(
});
},
isCompatible,
execute: async ({ filters, timeFieldName }: ApplyGlobalFilterActionContext) => {
execute: async ({ filters, timeFieldName }: ApplyFilterTriggerContext) => {
if (!filters) {
throw new Error('Applying a filter requires a filter');
}
Expand Down
88 changes: 61 additions & 27 deletions src/plugins/data/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
import './index.scss';

import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
PackageInfo,
Plugin,
PluginInitializerContext,
} from 'src/core/public';
import { ConfigSchema } from '../config';
import { Storage, IStorageWrapper, createStartServicesGetter } from '../../kibana_utils/public';
import { createStartServicesGetter, IStorageWrapper, Storage } from '../../kibana_utils/public';
import {
DataPublicPluginSetup,
DataPublicPluginStart,
Expand Down Expand Up @@ -55,31 +55,24 @@ import {
import { createSearchBar } from './ui/search_bar/create_search_bar';
import { esaggs } from './search/expressions';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
APPLY_FILTER_TRIGGER,
ApplyFilterTriggerContext,
VALUE_CLICK_TRIGGER,
SELECT_RANGE_TRIGGER,
} from '../../ui_actions/public';
import {
ACTION_GLOBAL_APPLY_FILTER,
createFilterAction,
createFiltersFromValueClickAction,
createFiltersFromRangeSelectAction,
createFiltersFromValueClickAction,
} from './actions';
import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action';
import {
selectRangeAction,
SelectRangeActionContext,
ACTION_SELECT_RANGE,
} from './actions/select_range_action';
import {
valueClickAction,
ACTION_VALUE_CLICK,
ValueClickActionContext,
} from './actions/value_click_action';
import { ACTION_SELECT_RANGE, SelectRangeActionContext } from './actions/select_range_action';
import { ACTION_VALUE_CLICK, ValueClickActionContext } from './actions/value_click_action';
import { ValueClickTriggerContext, RangeSelectTriggerContext } from '../../embeddable/public';

declare module '../../ui_actions/public' {
export interface ActionContextMapping {
[ACTION_GLOBAL_APPLY_FILTER]: ApplyGlobalFilterActionContext;
[ACTION_GLOBAL_APPLY_FILTER]: ApplyFilterTriggerContext;
[ACTION_SELECT_RANGE]: SelectRangeActionContext;
[ACTION_VALUE_CLICK]: ValueClickActionContext;
}
Expand Down Expand Up @@ -126,19 +119,60 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
storage: this.storage,
});

uiActions.registerAction(
uiActions.addTriggerAction(
APPLY_FILTER_TRIGGER,
createFilterAction(queryService.filterManager, queryService.timefilter.timefilter)
);

uiActions.addTriggerAction(
SELECT_RANGE_TRIGGER,
selectRangeAction(queryService.filterManager, queryService.timefilter.timefilter)
);
// uiActions.addTriggerAction(
// SELECT_RANGE_TRIGGER,
// selectRangeAction(queryService.filterManager, queryService.timefilter.timefilter)
// );
//
// uiActions.addTriggerAction(
// VALUE_CLICK_TRIGGER,
// valueClickAction(queryService.filterManager, queryService.timefilter.timefilter)
// );

uiActions.addTriggerAction(
VALUE_CLICK_TRIGGER,
valueClickAction(queryService.filterManager, queryService.timefilter.timefilter)
);
uiActions.registerTriggerReaction({
originTrigger: VALUE_CLICK_TRIGGER,
destTrigger: APPLY_FILTER_TRIGGER,
isCompatible: async (context: ValueClickTriggerContext) => {
const filters = await createFiltersFromValueClickAction(context.data);
if (filters.length > 0) return true;
return false;
},
originContextToDestContext: async (
context: ValueClickTriggerContext
): Promise<ApplyFilterTriggerContext> => {
const filters = await createFiltersFromValueClickAction(context.data);
return {
embeddable: context.embeddable,
filters,
timeFieldName: context.data.timeFieldName,
};
},
});

uiActions.registerTriggerReaction({
originTrigger: SELECT_RANGE_TRIGGER,
destTrigger: APPLY_FILTER_TRIGGER,
isCompatible: async (context: RangeSelectTriggerContext) => {
const filters = await createFiltersFromRangeSelectAction(context.data);
if (filters.length > 0) return true;
return false;
},
originContextToDestContext: async (
context: RangeSelectTriggerContext
): Promise<ApplyFilterTriggerContext> => {
const filters = await createFiltersFromRangeSelectAction(context.data);
return {
embeddable: context.embeddable,
filters,
timeFieldName: context.data.timeFieldName,
};
},
});

return {
autocomplete: this.autocomplete.setup(core),
Expand Down
6 changes: 4 additions & 2 deletions src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,10 @@ export class EmbeddablePanel extends React.Component<Props, State> {
const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField);

return await buildContextMenuForActions({
actions: sortedActions,
actionContext: { embeddable: this.props.embeddable },
actionsWithContext: sortedActions.map((action) => [
action,
{ embeddable: this.props.embeddable },
]),
closeMenu: this.closeMyContextMenuPanel,
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { uiToReactComponent } from '../../../kibana_react/public';
import { Action } from '../actions';
import { ActionContextTuple } from '../triggers';

export const defaultTitle = i18n.translate('uiActions.actionPanel.title', {
defaultMessage: 'Options',
Expand All @@ -32,19 +33,16 @@ export const defaultTitle = i18n.translate('uiActions.actionPanel.title', {
* Transforms an array of Actions to the shape EuiContextMenuPanel expects.
*/
export async function buildContextMenuForActions<Context extends object>({
actions,
actionContext,
actionsWithContext,
title = defaultTitle,
closeMenu,
}: {
actions: Array<Action<Context>>;
actionContext: Context;
actionsWithContext: ActionContextTuple[];
title?: string;
closeMenu: () => void;
}): Promise<EuiContextMenuPanelDescriptor> {
const menuItems = await buildEuiContextMenuPanelItems<Context>({
actions,
actionContext,
actionsWithContext,
closeMenu,
});

Expand All @@ -59,16 +57,14 @@ export async function buildContextMenuForActions<Context extends object>({
* Transform an array of Actions into the shape needed to build an EUIContextMenu
*/
async function buildEuiContextMenuPanelItems<Context extends object>({
actions,
actionContext,
actionsWithContext,
closeMenu,
}: {
actions: Array<Action<Context>>;
actionContext: Context;
actionsWithContext: ActionContextTuple[];
closeMenu: () => void;
}) {
const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length);
const promises = actions.map(async (action, index) => {
const items: EuiContextMenuPanelItemDescriptor[] = new Array(actionsWithContext.length);
const promises = actionsWithContext.map(async ([action, actionContext], index) => {
const isCompatible = await action.isCompatible(actionContext);
if (!isCompatible) {
return;
Expand Down
8 changes: 7 additions & 1 deletion src/plugins/ui_actions/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,11 @@ export {
APPLY_FILTER_TRIGGER,
applyFilterTrigger,
} from './triggers';
export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types';
export {
TriggerContextMapping,
TriggerId,
ActionContextMapping,
ActionType,
ApplyFilterTriggerContext,
} from './types';
export { ActionByType } from './actions';
1 change: 1 addition & 0 deletions src/plugins/ui_actions/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type UiActionsSetup = Pick<
| 'registerAction'
| 'registerTrigger'
| 'unregisterAction'
| 'registerTriggerReaction'
>;

export type UiActionsStart = PublicMethodsOf<UiActionsService>;
Expand Down
31 changes: 31 additions & 0 deletions src/plugins/ui_actions/public/service/ui_actions_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ import { Trigger, TriggerContext } from '../triggers/trigger';
import { TriggerInternal } from '../triggers/trigger_internal';
import { TriggerContract } from '../triggers/trigger_contract';

export interface TriggerReaction<
OriginTrigger extends TriggerId = TriggerId,
DestTrigger extends TriggerId = TriggerId
> {
originTrigger: OriginTrigger;
destTrigger: DestTrigger;
originContextToDestContext(
context: TriggerContextMapping[OriginTrigger]
): Promise<TriggerContextMapping[DestTrigger]>;
isCompatible(context: TriggerContextMapping[OriginTrigger]): Promise<boolean>;
}

export interface UiActionsServiceParams {
readonly triggers?: TriggerRegistry;
readonly actions?: ActionRegistry;
Expand All @@ -43,6 +55,7 @@ export class UiActionsService {
protected readonly triggers: TriggerRegistry;
protected readonly actions: ActionRegistry;
protected readonly triggerToActions: TriggerToActionsRegistry;
protected readonly triggerReactions = new Map<TriggerId, TriggerReaction[]>();

constructor({
triggers = new Map(),
Expand All @@ -63,8 +76,26 @@ export class UiActionsService {

this.triggers.set(trigger.id, triggerInternal);
this.triggerToActions.set(trigger.id, []);
this.triggerReactions.set(trigger.id, []);
};

public readonly registerTriggerReaction = (triggerReaction: TriggerReaction) => {
if (!this.triggers.has(triggerReaction.originTrigger)) {
throw new Error(`Trigger [trigger.id = ${triggerReaction.originTrigger}] not registered.`);
}
if (!this.triggers.has(triggerReaction.destTrigger)) {
throw new Error(`Trigger [trigger.id = ${triggerReaction.destTrigger}] not registered.`);
}

this.triggerReactions.set(triggerReaction.originTrigger, [
...this.triggerReactions.get(triggerReaction.originTrigger)!,
triggerReaction,
]);
};

public readonly getTriggerReactions = (triggerId: TriggerId) =>
Array.from(this.triggerReactions.get(triggerId) ?? []);

public readonly getTrigger = <T extends TriggerId>(triggerId: T): TriggerContract<T> => {
const trigger = this.triggers.get(triggerId);

Expand Down
44 changes: 29 additions & 15 deletions src/plugins/ui_actions/public/triggers/trigger_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { TriggerContract } from './trigger_contract';
import { UiActionsService } from '../service';
import { Action } from '../actions';
import { buildContextMenuForActions, openContextMenu } from '../context_menu';
import { TriggerId, TriggerContextMapping } from '../types';
import { TriggerId, TriggerContextMapping, BaseContext } from '../types';

export type ActionContextTuple = [Action, BaseContext];

/**
* Internal representation of a trigger kept for consumption only internally
Expand All @@ -37,34 +39,46 @@ export class TriggerInternal<T extends TriggerId> {
const triggerId = this.trigger.id;
const actions = await this.service.getTriggerCompatibleActions!(triggerId, context);

if (!actions.length) {
const actionsWithContexts: ActionContextTuple[] = actions.map((action) => [action, context]);

// TODO: make this recursive
const triggerReactions = this.service.getTriggerReactions(triggerId);
for (const reaction of triggerReactions) {
if (await reaction.isCompatible(context)) {
const reactionContext = await reaction.originContextToDestContext(context);
const reactionActions = await this.service.getTriggerCompatibleActions(
reaction.destTrigger,
reactionContext
);
const reactionActionsWithContext = reactionActions.map((reactionAction) => [
reactionAction,
reactionContext,
]);
actionsWithContexts.push(...(reactionActionsWithContext as ActionContextTuple[]));
}
}

if (!actionsWithContexts.length) {
throw new Error(
`No compatible actions found to execute for trigger [triggerId = ${triggerId}].`
);
}

if (actions.length === 1) {
await this.executeSingleAction(actions[0], context);
if (actionsWithContexts.length === 1) {
await this.executeSingleAction(actionsWithContexts[0]);
return;
}

await this.executeMultipleActions(actions, context);
await this.executeMultipleActions(actionsWithContexts);
}

private async executeSingleAction(
action: Action<TriggerContextMapping[T]>,
context: TriggerContextMapping[T]
) {
private async executeSingleAction([action, context]: ActionContextTuple) {
await action.execute(context);
}

private async executeMultipleActions(
actions: Array<Action<TriggerContextMapping[T]>>,
context: TriggerContextMapping[T]
) {
private async executeMultipleActions(actionsWithContext: ActionContextTuple[]) {
const panel = await buildContextMenuForActions({
actions,
actionContext: context,
actionsWithContext,
title: this.trigger.title,
closeMenu: () => session.close(),
});
Expand Down
11 changes: 7 additions & 4 deletions src/plugins/ui_actions/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ export type TriggerId = keyof TriggerContextMapping;
export type BaseContext = object;
export type TriggerContext = BaseContext;

export interface ApplyFilterTriggerContext<E extends IEmbeddable = IEmbeddable> {
embeddable?: IEmbeddable;
filters: Filter[];
timeFieldName?: string;
}

export interface TriggerContextMapping {
[DEFAULT_TRIGGER]: TriggerContext;
[SELECT_RANGE_TRIGGER]: RangeSelectTriggerContext;
[VALUE_CLICK_TRIGGER]: ValueClickTriggerContext;
[APPLY_FILTER_TRIGGER]: {
embeddable: IEmbeddable;
filters: Filter[];
};
[APPLY_FILTER_TRIGGER]: ApplyFilterTriggerContext;
}

const DEFAULT_ACTION = '';
Expand Down
Loading

0 comments on commit 4b4f9ff

Please sign in to comment.