Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC URL drilldown: trigger picker #70421

Closed
wants to merge 10 commits into from
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