mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[uiActions] Support emitting nested triggers and actions (#70602)
* Introduce automatically executed actions * Introduce batching of emitted triggers to be execute on the macro task
This commit is contained in:
parent
0173ef3528
commit
1ac56d7bfc
27 changed files with 368 additions and 215 deletions
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) > [embeddable](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.embeddable.md)
|
||||
|
||||
## ApplyGlobalFilterActionContext.embeddable property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
embeddable?: IEmbeddable;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) > [filters](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.filters.md)
|
||||
|
||||
## ApplyGlobalFilterActionContext.filters property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
filters: Filter[];
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md)
|
||||
|
||||
## ApplyGlobalFilterActionContext interface
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ApplyGlobalFilterActionContext
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [embeddable](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.embeddable.md) | <code>IEmbeddable</code> | |
|
||||
| [filters](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.filters.md) | <code>Filter[]</code> | |
|
||||
| [timeFieldName](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.timefieldname.md) | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) > [timeFieldName](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.timefieldname.md)
|
||||
|
||||
## ApplyGlobalFilterActionContext.timeFieldName property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
timeFieldName?: string;
|
||||
```
|
|
@ -48,6 +48,7 @@
|
|||
| Interface | Description |
|
||||
| --- | --- |
|
||||
| [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | |
|
||||
| [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) | |
|
||||
| [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | |
|
||||
| [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | |
|
||||
| [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | |
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
setup(core: CoreSetup, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
setup(core: CoreSetup<DataStartDependencies, DataPublicPluginStart>, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| core | <code>CoreSetup</code> | |
|
||||
| core | <code>CoreSetup<DataStartDependencies, DataPublicPluginStart></code> | |
|
||||
| { expressions, uiActions, usageCollection } | <code>DataSetupDependencies</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
|
|
@ -31,7 +31,7 @@ export const ACTION_VIEW_IN_MAPS = 'ACTION_VIEW_IN_MAPS';
|
|||
export const ACTION_TRAVEL_GUIDE = 'ACTION_TRAVEL_GUIDE';
|
||||
export const ACTION_CALL_PHONE_NUMBER = 'ACTION_CALL_PHONE_NUMBER';
|
||||
export const ACTION_EDIT_USER = 'ACTION_EDIT_USER';
|
||||
export const ACTION_PHONE_USER = 'ACTION_PHONE_USER';
|
||||
export const ACTION_TRIGGER_PHONE_USER = 'ACTION_TRIGGER_PHONE_USER';
|
||||
export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY';
|
||||
|
||||
export const showcasePluggability = createAction<typeof ACTION_SHOWCASE_PLUGGABILITY>({
|
||||
|
@ -120,19 +120,13 @@ export interface UserContext {
|
|||
update: (user: User) => void;
|
||||
}
|
||||
|
||||
export const createPhoneUserAction = (getUiActionsApi: () => Promise<UiActionsStart>) =>
|
||||
createAction<typeof ACTION_PHONE_USER>({
|
||||
type: ACTION_PHONE_USER,
|
||||
export const createTriggerPhoneTriggerAction = (getUiActionsApi: () => Promise<UiActionsStart>) =>
|
||||
createAction<typeof ACTION_TRIGGER_PHONE_USER>({
|
||||
type: ACTION_TRIGGER_PHONE_USER,
|
||||
getDisplayName: () => 'Call phone number',
|
||||
shouldAutoExecute: async () => true,
|
||||
isCompatible: async ({ user }) => user.phone !== undefined,
|
||||
execute: async ({ user }) => {
|
||||
// One option - execute the more specific action directly.
|
||||
// makePhoneCallAction.execute({ phone: user.phone });
|
||||
|
||||
// Another option - emit the trigger and automatically get *all* the actions attached
|
||||
// to the phone number trigger.
|
||||
// TODO: we need to figure out the best way to handle these nested actions however, since
|
||||
// we don't want multiple context menu's to pop up.
|
||||
if (user.phone !== undefined) {
|
||||
(await getUiActionsApi()).executeTriggerActions(PHONE_TRIGGER, { phone: user.phone });
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
PHONE_TRIGGER,
|
||||
USER_TRIGGER,
|
||||
COUNTRY_TRIGGER,
|
||||
createPhoneUserAction,
|
||||
lookUpWeatherAction,
|
||||
viewInMapsAction,
|
||||
createEditUserAction,
|
||||
|
@ -37,7 +36,8 @@ import {
|
|||
ACTION_CALL_PHONE_NUMBER,
|
||||
ACTION_TRAVEL_GUIDE,
|
||||
ACTION_VIEW_IN_MAPS,
|
||||
ACTION_PHONE_USER,
|
||||
ACTION_TRIGGER_PHONE_USER,
|
||||
createTriggerPhoneTriggerAction,
|
||||
} from './actions/actions';
|
||||
import { DeveloperExamplesSetup } from '../../developer_examples/public';
|
||||
import image from './ui_actions.png';
|
||||
|
@ -64,7 +64,7 @@ declare module '../../../src/plugins/ui_actions/public' {
|
|||
[ACTION_CALL_PHONE_NUMBER]: PhoneContext;
|
||||
[ACTION_TRAVEL_GUIDE]: CountryContext;
|
||||
[ACTION_VIEW_IN_MAPS]: CountryContext;
|
||||
[ACTION_PHONE_USER]: UserContext;
|
||||
[ACTION_TRIGGER_PHONE_USER]: UserContext;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ export class UiActionsExplorerPlugin implements Plugin<void, void, {}, StartDeps
|
|||
|
||||
deps.uiActions.addTriggerAction(
|
||||
USER_TRIGGER,
|
||||
createPhoneUserAction(async () => (await startServices)[1].uiActions)
|
||||
createTriggerPhoneTriggerAction(async () => (await startServices)[1].uiActions)
|
||||
);
|
||||
deps.uiActions.addTriggerAction(
|
||||
USER_TRIGGER,
|
||||
|
|
|
@ -22,6 +22,7 @@ import { toMountPoint } from '../../../kibana_react/public';
|
|||
import { ActionByType, createAction, IncompatibleActionError } from '../../../ui_actions/public';
|
||||
import { getOverlays, getIndexPatterns } from '../services';
|
||||
import { applyFiltersPopover } from '../ui/apply_filters';
|
||||
import type { IEmbeddable } from '../../../embeddable/public';
|
||||
import { Filter, FilterManager, TimefilterContract, esFilters } from '..';
|
||||
|
||||
export const ACTION_GLOBAL_APPLY_FILTER = 'ACTION_GLOBAL_APPLY_FILTER';
|
||||
|
@ -29,6 +30,7 @@ export const ACTION_GLOBAL_APPLY_FILTER = 'ACTION_GLOBAL_APPLY_FILTER';
|
|||
export interface ApplyGlobalFilterActionContext {
|
||||
filters: Filter[];
|
||||
timeFieldName?: string;
|
||||
embeddable?: IEmbeddable;
|
||||
}
|
||||
|
||||
async function isCompatible(context: ApplyGlobalFilterActionContext) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import moment from 'moment';
|
|||
import { esFilters, IFieldType, RangeFilterParams } from '../../../public';
|
||||
import { getIndexPatterns } from '../../../public/services';
|
||||
import { deserializeAggConfig } from '../../search/expressions/utils';
|
||||
import { RangeSelectContext } from '../../../../embeddable/public';
|
||||
import type { RangeSelectContext } from '../../../../embeddable/public';
|
||||
|
||||
export async function createFiltersFromRangeSelectAction(event: RangeSelectContext['data']) {
|
||||
const column: Record<string, any> = event.table.columns[event.column];
|
||||
|
|
|
@ -21,7 +21,7 @@ import { KibanaDatatable } from '../../../../../plugins/expressions/public';
|
|||
import { deserializeAggConfig } from '../../search/expressions';
|
||||
import { esFilters, Filter } from '../../../public';
|
||||
import { getIndexPatterns } from '../../../public/services';
|
||||
import { ValueClickContext } from '../../../../embeddable/public';
|
||||
import type { ValueClickContext } from '../../../../embeddable/public';
|
||||
|
||||
/**
|
||||
* For terms aggregations on `__other__` buckets, this assembles a list of applicable filter
|
||||
|
|
|
@ -17,8 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { ACTION_GLOBAL_APPLY_FILTER, createFilterAction } from './apply_filter_action';
|
||||
export {
|
||||
ACTION_GLOBAL_APPLY_FILTER,
|
||||
createFilterAction,
|
||||
ApplyGlobalFilterActionContext,
|
||||
} from './apply_filter_action';
|
||||
export { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click';
|
||||
export { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select';
|
||||
export { selectRangeAction } from './select_range_action';
|
||||
export { valueClickAction } from './value_click_action';
|
||||
export * from './select_range_action';
|
||||
export * from './value_click_action';
|
||||
|
|
|
@ -17,60 +17,39 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
createAction,
|
||||
IncompatibleActionError,
|
||||
ActionByType,
|
||||
APPLY_FILTER_TRIGGER,
|
||||
createAction,
|
||||
UiActionsStart,
|
||||
} from '../../../../plugins/ui_actions/public';
|
||||
import { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select';
|
||||
import { RangeSelectContext } from '../../../embeddable/public';
|
||||
import { FilterManager, TimefilterContract, esFilters } from '..';
|
||||
|
||||
export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE';
|
||||
import type { RangeSelectContext } from '../../../embeddable/public';
|
||||
|
||||
export type SelectRangeActionContext = RangeSelectContext;
|
||||
|
||||
async function isCompatible(context: SelectRangeActionContext) {
|
||||
try {
|
||||
return Boolean(await createFiltersFromRangeSelectAction(context.data));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE';
|
||||
|
||||
export function selectRangeAction(
|
||||
filterManager: FilterManager,
|
||||
timeFilter: TimefilterContract
|
||||
export function createSelectRangeAction(
|
||||
getStartServices: () => { uiActions: UiActionsStart }
|
||||
): ActionByType<typeof ACTION_SELECT_RANGE> {
|
||||
return createAction<typeof ACTION_SELECT_RANGE>({
|
||||
type: ACTION_SELECT_RANGE,
|
||||
id: ACTION_SELECT_RANGE,
|
||||
getIconType: () => 'filter',
|
||||
getDisplayName: () => {
|
||||
return i18n.translate('data.filter.applyFilterActionTitle', {
|
||||
defaultMessage: 'Apply filter to current view',
|
||||
});
|
||||
},
|
||||
isCompatible,
|
||||
execute: async ({ data }: SelectRangeActionContext) => {
|
||||
if (!(await isCompatible({ data }))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
||||
const selectedFilters = await createFiltersFromRangeSelectAction(data);
|
||||
|
||||
if (data.timeFieldName) {
|
||||
const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
|
||||
data.timeFieldName,
|
||||
selectedFilters
|
||||
);
|
||||
filterManager.addFilters(restOfFilters);
|
||||
if (timeRangeFilter) {
|
||||
esFilters.changeTimeFilter(timeFilter, timeRangeFilter);
|
||||
shouldAutoExecute: async () => true,
|
||||
execute: async (context: SelectRangeActionContext) => {
|
||||
try {
|
||||
const filters = await createFiltersFromRangeSelectAction(context.data);
|
||||
if (filters.length > 0) {
|
||||
await getStartServices().uiActions.getTrigger(APPLY_FILTER_TRIGGER).exec({
|
||||
filters,
|
||||
embeddable: context.embeddable,
|
||||
timeFieldName: context.data.timeFieldName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
filterManager.addFilters(selectedFilters);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Error [ACTION_SELECT_RANGE]: can\'t extract filters from action context`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,98 +17,41 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '../../../../plugins/kibana_react/public';
|
||||
import {
|
||||
ActionByType,
|
||||
APPLY_FILTER_TRIGGER,
|
||||
createAction,
|
||||
IncompatibleActionError,
|
||||
UiActionsStart,
|
||||
} from '../../../../plugins/ui_actions/public';
|
||||
import { getOverlays, getIndexPatterns } from '../services';
|
||||
import { applyFiltersPopover } from '../ui/apply_filters';
|
||||
import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click';
|
||||
import { ValueClickContext } from '../../../embeddable/public';
|
||||
import { Filter, FilterManager, TimefilterContract, esFilters } from '..';
|
||||
|
||||
export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK';
|
||||
import type { Filter } from '../../common/es_query/filters';
|
||||
import type { ValueClickContext } from '../../../embeddable/public';
|
||||
|
||||
export type ValueClickActionContext = ValueClickContext;
|
||||
export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK';
|
||||
|
||||
async function isCompatible(context: ValueClickActionContext) {
|
||||
try {
|
||||
const filters: Filter[] = await createFiltersFromValueClickAction(context.data);
|
||||
return filters.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function valueClickAction(
|
||||
filterManager: FilterManager,
|
||||
timeFilter: TimefilterContract
|
||||
export function createValueClickAction(
|
||||
getStartServices: () => { uiActions: UiActionsStart }
|
||||
): ActionByType<typeof ACTION_VALUE_CLICK> {
|
||||
return createAction<typeof ACTION_VALUE_CLICK>({
|
||||
type: ACTION_VALUE_CLICK,
|
||||
id: ACTION_VALUE_CLICK,
|
||||
getIconType: () => 'filter',
|
||||
getDisplayName: () => {
|
||||
return i18n.translate('data.filter.applyFilterActionTitle', {
|
||||
defaultMessage: 'Apply filter to current view',
|
||||
});
|
||||
},
|
||||
isCompatible,
|
||||
execute: async ({ data }: ValueClickActionContext) => {
|
||||
if (!(await isCompatible({ data }))) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
||||
const filters: Filter[] = await createFiltersFromValueClickAction(data);
|
||||
|
||||
let selectedFilters = filters;
|
||||
|
||||
if (filters.length > 1) {
|
||||
const indexPatterns = await Promise.all(
|
||||
filters.map((filter) => {
|
||||
return getIndexPatterns().get(filter.meta.index!);
|
||||
})
|
||||
);
|
||||
|
||||
const filterSelectionPromise: Promise<Filter[]> = new Promise((resolve) => {
|
||||
const overlay = getOverlays().openModal(
|
||||
toMountPoint(
|
||||
applyFiltersPopover(
|
||||
filters,
|
||||
indexPatterns,
|
||||
() => {
|
||||
overlay.close();
|
||||
resolve([]);
|
||||
},
|
||||
(filterSelection: Filter[]) => {
|
||||
overlay.close();
|
||||
resolve(filterSelection);
|
||||
}
|
||||
)
|
||||
),
|
||||
{
|
||||
'data-test-subj': 'selectFilterOverlay',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
selectedFilters = await filterSelectionPromise;
|
||||
}
|
||||
|
||||
if (data.timeFieldName) {
|
||||
const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter(
|
||||
data.timeFieldName,
|
||||
selectedFilters
|
||||
);
|
||||
filterManager.addFilters(restOfFilters);
|
||||
if (timeRangeFilter) {
|
||||
esFilters.changeTimeFilter(timeFilter, timeRangeFilter);
|
||||
shouldAutoExecute: async () => true,
|
||||
execute: async (context: ValueClickActionContext) => {
|
||||
try {
|
||||
const filters: Filter[] = await createFiltersFromValueClickAction(context.data);
|
||||
if (filters.length > 0) {
|
||||
await getStartServices().uiActions.getTrigger(APPLY_FILTER_TRIGGER).exec({
|
||||
filters,
|
||||
embeddable: context.embeddable,
|
||||
timeFieldName: context.data.timeFieldName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
filterManager.addFilters(selectedFilters);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Error [ACTION_EMIT_APPLY_FILTER_TRIGGER]: can\'t extract filters from action context`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -438,6 +438,8 @@ export {
|
|||
|
||||
export { isTimeRange, isQuery, isFilter, isFilters } from '../common';
|
||||
|
||||
export { ApplyGlobalFilterActionContext } from './actions';
|
||||
|
||||
export * from '../common/field_mapping';
|
||||
|
||||
/*
|
||||
|
|
|
@ -69,18 +69,15 @@ import {
|
|||
createFilterAction,
|
||||
createFiltersFromValueClickAction,
|
||||
createFiltersFromRangeSelectAction,
|
||||
} from './actions';
|
||||
import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action';
|
||||
import {
|
||||
selectRangeAction,
|
||||
SelectRangeActionContext,
|
||||
ApplyGlobalFilterActionContext,
|
||||
ACTION_SELECT_RANGE,
|
||||
} from './actions/select_range_action';
|
||||
import {
|
||||
valueClickAction,
|
||||
ACTION_VALUE_CLICK,
|
||||
SelectRangeActionContext,
|
||||
ValueClickActionContext,
|
||||
} from './actions/value_click_action';
|
||||
createValueClickAction,
|
||||
createSelectRangeAction,
|
||||
} from './actions';
|
||||
|
||||
import { SavedObjectsClientPublicToCommon } from './index_patterns';
|
||||
import { indexPatternLoad } from './index_patterns/expressions/load_index_pattern';
|
||||
|
||||
|
@ -92,7 +89,14 @@ declare module '../../ui_actions/public' {
|
|||
}
|
||||
}
|
||||
|
||||
export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPublicPluginStart> {
|
||||
export class DataPublicPlugin
|
||||
implements
|
||||
Plugin<
|
||||
DataPublicPluginSetup,
|
||||
DataPublicPluginStart,
|
||||
DataSetupDependencies,
|
||||
DataStartDependencies
|
||||
> {
|
||||
private readonly autocomplete: AutocompleteService;
|
||||
private readonly searchService: SearchService;
|
||||
private readonly fieldFormatsService: FieldFormatsService;
|
||||
|
@ -110,13 +114,13 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
|
|||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
core: CoreSetup<DataStartDependencies, DataPublicPluginStart>,
|
||||
{ expressions, uiActions, usageCollection }: DataSetupDependencies
|
||||
): DataPublicPluginSetup {
|
||||
const startServices = createStartServicesGetter(core.getStartServices);
|
||||
|
||||
const getInternalStartServices = (): InternalStartServices => {
|
||||
const { core: coreStart, self }: any = startServices();
|
||||
const { core: coreStart, self } = startServices();
|
||||
return {
|
||||
fieldFormats: self.fieldFormats,
|
||||
notifications: coreStart.notifications,
|
||||
|
@ -140,12 +144,16 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
|
|||
|
||||
uiActions.addTriggerAction(
|
||||
SELECT_RANGE_TRIGGER,
|
||||
selectRangeAction(queryService.filterManager, queryService.timefilter.timefilter)
|
||||
createSelectRangeAction(() => ({
|
||||
uiActions: startServices().plugins.uiActions,
|
||||
}))
|
||||
);
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
VALUE_CLICK_TRIGGER,
|
||||
valueClickAction(queryService.filterManager, queryService.timefilter.timefilter)
|
||||
createValueClickAction(() => ({
|
||||
uiActions: startServices().plugins.uiActions,
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -250,6 +250,20 @@ export class AggParamType<TAggConfig extends IAggConfig = IAggConfig> extends Ba
|
|||
makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ApplyGlobalFilterActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface ApplyGlobalFilterActionContext {
|
||||
// Warning: (ae-forgotten-export) The symbol "IEmbeddable" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
embeddable?: IEmbeddable;
|
||||
// (undocumented)
|
||||
filters: Filter[];
|
||||
// (undocumented)
|
||||
timeFieldName?: string;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
@ -1443,18 +1457,16 @@ export type PhrasesFilter = Filter & {
|
|||
meta: PhrasesFilterMeta;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "DataPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export class Plugin implements Plugin_2<DataPublicPluginSetup, DataPublicPluginStart> {
|
||||
export class Plugin implements Plugin_2<DataPublicPluginSetup, DataPublicPluginStart, DataSetupDependencies, DataStartDependencies> {
|
||||
// Warning: (ae-forgotten-export) The symbol "ConfigSchema" needs to be exported by the entry point index.d.ts
|
||||
constructor(initializerContext: PluginInitializerContext_2<ConfigSchema>);
|
||||
// Warning: (ae-forgotten-export) The symbol "DataSetupDependencies" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
setup(core: CoreSetup, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
// Warning: (ae-forgotten-export) The symbol "DataStartDependencies" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
setup(core: CoreSetup<DataStartDependencies, DataPublicPluginStart>, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
|
||||
// (undocumented)
|
||||
start(core: CoreStart_2, { uiActions }: DataStartDependencies): DataPublicPluginStart;
|
||||
// (undocumented)
|
||||
|
|
|
@ -311,8 +311,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField);
|
||||
|
||||
return await buildContextMenuForActions({
|
||||
actions: sortedActions,
|
||||
actionContext: { embeddable: this.props.embeddable },
|
||||
actions: sortedActions.map((action) => [action, { embeddable: this.props.embeddable }]),
|
||||
closeMenu: this.closeMyContextMenuPanel,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"public/tests/test_samples"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaUtils",
|
||||
"kibanaReact"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -68,6 +68,13 @@ export interface Action<Context extends {} = {}, T = ActionType>
|
|||
* Executes the action.
|
||||
*/
|
||||
execute(context: Context): Promise<void>;
|
||||
|
||||
/**
|
||||
* Determines if action should be executed automatically,
|
||||
* without first showing up in context menu.
|
||||
* false by default.
|
||||
*/
|
||||
shouldAutoExecute?(context: Context): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,6 +96,13 @@ export interface ActionDefinition<Context extends object = object>
|
|||
* Executes the action.
|
||||
*/
|
||||
execute(context: Context): Promise<void>;
|
||||
|
||||
/**
|
||||
* Determines if action should be executed automatically,
|
||||
* without first showing up in context menu.
|
||||
* false by default.
|
||||
*/
|
||||
shouldAutoExecute?(context: Context): Promise<boolean>;
|
||||
}
|
||||
|
||||
export type ActionContext<A> = A extends ActionDefinition<infer Context> ? Context : never;
|
||||
|
|
|
@ -65,4 +65,9 @@ export class ActionInternal<A extends ActionDefinition = ActionDefinition>
|
|||
if (!this.definition.getHref) return undefined;
|
||||
return await this.definition.getHref(context);
|
||||
}
|
||||
|
||||
public async shouldAutoExecute(context: Context<A>): Promise<boolean> {
|
||||
if (!this.definition.shouldAutoExecute) return false;
|
||||
return this.definition.shouldAutoExecute(context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,28 +23,28 @@ import _ from 'lodash';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { uiToReactComponent } from '../../../kibana_react/public';
|
||||
import { Action } from '../actions';
|
||||
import { BaseContext } from '../types';
|
||||
|
||||
export const defaultTitle = i18n.translate('uiActions.actionPanel.title', {
|
||||
defaultMessage: 'Options',
|
||||
});
|
||||
|
||||
type ActionWithContext<Context extends BaseContext = BaseContext> = [Action<Context>, Context];
|
||||
|
||||
/**
|
||||
* Transforms an array of Actions to the shape EuiContextMenuPanel expects.
|
||||
*/
|
||||
export async function buildContextMenuForActions<Context extends object>({
|
||||
export async function buildContextMenuForActions({
|
||||
actions,
|
||||
actionContext,
|
||||
title = defaultTitle,
|
||||
closeMenu,
|
||||
}: {
|
||||
actions: Array<Action<Context>>;
|
||||
actionContext: Context;
|
||||
actions: ActionWithContext[];
|
||||
title?: string;
|
||||
closeMenu: () => void;
|
||||
}): Promise<EuiContextMenuPanelDescriptor> {
|
||||
const menuItems = await buildEuiContextMenuPanelItems<Context>({
|
||||
const menuItems = await buildEuiContextMenuPanelItems({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
});
|
||||
|
||||
|
@ -58,17 +58,15 @@ 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>({
|
||||
async function buildEuiContextMenuPanelItems({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
}: {
|
||||
actions: Array<Action<Context>>;
|
||||
actionContext: Context;
|
||||
actions: ActionWithContext[];
|
||||
closeMenu: () => void;
|
||||
}) {
|
||||
const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length);
|
||||
const promises = actions.map(async (action, index) => {
|
||||
const promises = actions.map(async ([action, actionContext], index) => {
|
||||
const isCompatible = await action.isCompatible(actionContext);
|
||||
if (!isCompatible) {
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 { uniqBy } from 'lodash';
|
||||
import { Action } from '../actions';
|
||||
import { BaseContext } from '../types';
|
||||
import { defer as createDefer, Defer } from '../../../kibana_utils/public';
|
||||
import { buildContextMenuForActions, openContextMenu } from '../context_menu';
|
||||
import { Trigger } from '../triggers';
|
||||
|
||||
interface ExecuteActionTask {
|
||||
action: Action;
|
||||
context: BaseContext;
|
||||
trigger: Trigger;
|
||||
defer: Defer<void>;
|
||||
}
|
||||
|
||||
export class UiActionsExecutionService {
|
||||
private readonly batchingQueue: ExecuteActionTask[] = [];
|
||||
private readonly pendingTasks = new Set<ExecuteActionTask>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
async execute({
|
||||
action,
|
||||
context,
|
||||
trigger,
|
||||
}: {
|
||||
action: Action<BaseContext>;
|
||||
context: BaseContext;
|
||||
trigger: Trigger;
|
||||
}): Promise<void> {
|
||||
const shouldBatch = !(await action.shouldAutoExecute?.(context)) ?? false;
|
||||
const task: ExecuteActionTask = {
|
||||
action,
|
||||
context,
|
||||
trigger,
|
||||
defer: createDefer(),
|
||||
};
|
||||
|
||||
if (shouldBatch) {
|
||||
this.batchingQueue.push(task);
|
||||
} else {
|
||||
this.pendingTasks.add(task);
|
||||
try {
|
||||
await action.execute(context);
|
||||
this.pendingTasks.delete(task);
|
||||
} catch (e) {
|
||||
this.pendingTasks.delete(task);
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduleFlush();
|
||||
|
||||
return task.defer.promise;
|
||||
}
|
||||
|
||||
private scheduleFlush() {
|
||||
/**
|
||||
* Have to delay at least until next macro task
|
||||
* Otherwise chain:
|
||||
* Trigger -> await action.execute() -> trigger -> action
|
||||
* isn't batched
|
||||
*
|
||||
* This basically needed to support a chain of scheduled micro tasks (async/awaits) within uiActions code
|
||||
*/
|
||||
setTimeout(() => {
|
||||
if (this.pendingTasks.size === 0) {
|
||||
const tasks = uniqBy(this.batchingQueue, (t) => t.action.id);
|
||||
if (tasks.length === 1) {
|
||||
this.executeSingleTask(tasks[0]);
|
||||
}
|
||||
if (tasks.length > 1) {
|
||||
this.executeMultipleActions(tasks);
|
||||
}
|
||||
|
||||
this.batchingQueue.splice(0, this.batchingQueue.length);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private async executeSingleTask({ context, action, defer }: ExecuteActionTask) {
|
||||
try {
|
||||
await action.execute(context);
|
||||
defer.resolve();
|
||||
} catch (e) {
|
||||
defer.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeMultipleActions(tasks: ExecuteActionTask[]) {
|
||||
const panel = await buildContextMenuForActions({
|
||||
actions: tasks.map(({ action, context }) => [action, context]),
|
||||
title: tasks[0].trigger.title, // title of context menu is title of trigger which originated the chain
|
||||
closeMenu: () => {
|
||||
tasks.forEach((t) => t.defer.resolve());
|
||||
session.close();
|
||||
},
|
||||
});
|
||||
const session = openContextMenu([panel], {
|
||||
'data-test-subj': 'multipleActionsContextMenu',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import { ActionInternal, Action, ActionDefinition, ActionContext } from '../acti
|
|||
import { Trigger, TriggerContext } from '../triggers/trigger';
|
||||
import { TriggerInternal } from '../triggers/trigger_internal';
|
||||
import { TriggerContract } from '../triggers/trigger_contract';
|
||||
import { UiActionsExecutionService } from './ui_actions_execution_service';
|
||||
|
||||
export interface UiActionsServiceParams {
|
||||
readonly triggers?: TriggerRegistry;
|
||||
|
@ -40,6 +41,7 @@ export interface UiActionsServiceParams {
|
|||
}
|
||||
|
||||
export class UiActionsService {
|
||||
public readonly executionService = new UiActionsExecutionService();
|
||||
protected readonly triggers: TriggerRegistry;
|
||||
protected readonly actions: ActionRegistry;
|
||||
protected readonly triggerToActions: TriggerToActionsRegistry;
|
||||
|
|
|
@ -22,6 +22,7 @@ import { openContextMenu } from '../context_menu';
|
|||
import { uiActionsPluginMock } from '../mocks';
|
||||
import { Trigger } from '../triggers';
|
||||
import { TriggerId, ActionType } from '../types';
|
||||
import { wait } from '@testing-library/dom';
|
||||
|
||||
jest.mock('../context_menu');
|
||||
|
||||
|
@ -36,13 +37,15 @@ const TEST_ACTION_TYPE = 'TEST_ACTION_TYPE' as ActionType;
|
|||
|
||||
function createTestAction<C extends object>(
|
||||
type: string,
|
||||
checkCompatibility: (context: C) => boolean
|
||||
checkCompatibility: (context: C) => boolean,
|
||||
autoExecutable = false
|
||||
): Action<object> {
|
||||
return createAction<typeof TEST_ACTION_TYPE>({
|
||||
type: type as ActionType,
|
||||
id: type,
|
||||
isCompatible: (context: C) => Promise.resolve(checkCompatibility(context)),
|
||||
execute: (context) => executeFn(context),
|
||||
shouldAutoExecute: () => Promise.resolve(autoExecutable),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -57,6 +60,7 @@ const reset = () => {
|
|||
|
||||
executeFn.mockReset();
|
||||
openContextMenuSpy.mockReset();
|
||||
jest.useFakeTimers();
|
||||
};
|
||||
beforeEach(reset);
|
||||
|
||||
|
@ -75,6 +79,8 @@ test('executes a single action mapped to a trigger', async () => {
|
|||
const start = doStart();
|
||||
await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(executeFn).toBeCalledTimes(1);
|
||||
expect(executeFn).toBeCalledWith(context);
|
||||
});
|
||||
|
@ -117,6 +123,8 @@ test('does not execute an incompatible action', async () => {
|
|||
};
|
||||
await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(executeFn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
|
@ -139,8 +147,12 @@ test('shows a context menu when more than one action is mapped to a trigger', as
|
|||
const context = {};
|
||||
await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context);
|
||||
|
||||
expect(executeFn).toBeCalledTimes(0);
|
||||
expect(openContextMenu).toHaveBeenCalledTimes(1);
|
||||
jest.runAllTimers();
|
||||
|
||||
await wait(() => {
|
||||
expect(executeFn).toBeCalledTimes(0);
|
||||
expect(openContextMenu).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('passes whole action context to isCompatible()', async () => {
|
||||
|
@ -161,4 +173,32 @@ test('passes whole action context to isCompatible()', async () => {
|
|||
|
||||
const context = { foo: 'bar' };
|
||||
await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
test("doesn't show a context menu for auto executable actions", async () => {
|
||||
const { setup, doStart } = uiActions;
|
||||
const trigger: Trigger = {
|
||||
id: 'MY-TRIGGER' as TriggerId,
|
||||
title: 'My trigger',
|
||||
};
|
||||
const action1 = createTestAction('test1', () => true, true);
|
||||
const action2 = createTestAction('test2', () => true, false);
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.addTriggerAction(trigger.id, action1);
|
||||
setup.addTriggerAction(trigger.id, action2);
|
||||
|
||||
expect(openContextMenu).toHaveBeenCalledTimes(0);
|
||||
|
||||
const start = doStart();
|
||||
const context = {};
|
||||
await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await wait(() => {
|
||||
expect(executeFn).toBeCalledTimes(2);
|
||||
expect(openContextMenu).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
import { Trigger } from './trigger';
|
||||
import { TriggerContract } from './trigger_contract';
|
||||
import { UiActionsService } from '../service';
|
||||
import { Action } from '../actions';
|
||||
import { buildContextMenuForActions, openContextMenu } from '../context_menu';
|
||||
import { TriggerId, TriggerContextMapping } from '../types';
|
||||
|
||||
/**
|
||||
|
@ -43,33 +41,14 @@ export class TriggerInternal<T extends TriggerId> {
|
|||
);
|
||||
}
|
||||
|
||||
if (actions.length === 1) {
|
||||
await this.executeSingleAction(actions[0], context);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.executeMultipleActions(actions, context);
|
||||
}
|
||||
|
||||
private async executeSingleAction(
|
||||
action: Action<TriggerContextMapping[T]>,
|
||||
context: TriggerContextMapping[T]
|
||||
) {
|
||||
await action.execute(context);
|
||||
}
|
||||
|
||||
private async executeMultipleActions(
|
||||
actions: Array<Action<TriggerContextMapping[T]>>,
|
||||
context: TriggerContextMapping[T]
|
||||
) {
|
||||
const panel = await buildContextMenuForActions({
|
||||
actions,
|
||||
actionContext: context,
|
||||
title: this.trigger.title,
|
||||
closeMenu: () => session.close(),
|
||||
});
|
||||
const session = openContextMenu([panel], {
|
||||
'data-test-subj': 'multipleActionsContextMenu',
|
||||
});
|
||||
await Promise.all([
|
||||
actions.map((action) =>
|
||||
this.service.executionService.execute({
|
||||
action,
|
||||
context,
|
||||
trigger: this.trigger,
|
||||
})
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,9 @@
|
|||
|
||||
import { ActionInternal } from './actions/action_internal';
|
||||
import { TriggerInternal } from './triggers/trigger_internal';
|
||||
import { Filter } from '../../data/public';
|
||||
import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers';
|
||||
import { IEmbeddable } from '../../embeddable/public';
|
||||
import { RangeSelectContext, ValueClickContext } from '../../embeddable/public';
|
||||
import type { RangeSelectContext, ValueClickContext } from '../../embeddable/public';
|
||||
import type { ApplyGlobalFilterActionContext } from '../../data/public';
|
||||
|
||||
export type TriggerRegistry = Map<TriggerId, TriggerInternal<any>>;
|
||||
export type ActionRegistry = Map<string, ActionInternal>;
|
||||
|
@ -39,10 +38,7 @@ export interface TriggerContextMapping {
|
|||
[DEFAULT_TRIGGER]: TriggerContext;
|
||||
[SELECT_RANGE_TRIGGER]: RangeSelectContext;
|
||||
[VALUE_CLICK_TRIGGER]: ValueClickContext;
|
||||
[APPLY_FILTER_TRIGGER]: {
|
||||
embeddable: IEmbeddable;
|
||||
filters: Filter[];
|
||||
};
|
||||
[APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext;
|
||||
}
|
||||
|
||||
const DEFAULT_ACTION = '';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue