mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Drilldowns] Trigger picker (#74751)
Drilldowns now support trigger picker. It allows to create a drilldown and specify which trigger it should be attached to. Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
fcb1a2848a
commit
3c5f2e7e7b
47 changed files with 942 additions and 150 deletions
|
@ -115,7 +115,7 @@ export class UiActionsExecutionService {
|
|||
context,
|
||||
trigger,
|
||||
})),
|
||||
title: tasks[0].trigger.title, // title of context menu is title of trigger which originated the chain
|
||||
title: '', // intentionally don't have any title
|
||||
closeMenu: () => {
|
||||
tasks.forEach((t) => t.defer.resolve());
|
||||
session.close();
|
||||
|
|
|
@ -17,11 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Trigger } from '.';
|
||||
|
||||
export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER';
|
||||
export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = {
|
||||
id: APPLY_FILTER_TRIGGER,
|
||||
title: 'Apply filter',
|
||||
description: 'Triggered when user applies filter to an embeddable.',
|
||||
title: i18n.translate('uiActions.triggers.applyFilterTitle', {
|
||||
defaultMessage: 'Apply filter',
|
||||
}),
|
||||
description: i18n.translate('uiActions.triggers.applyFilterDescription', {
|
||||
defaultMessage: 'When kibana filter is applied. Could be a single value or a range filter.',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -17,13 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Trigger } from '.';
|
||||
|
||||
export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER';
|
||||
export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = {
|
||||
id: SELECT_RANGE_TRIGGER,
|
||||
// This is empty string to hide title of ui_actions context menu that appears
|
||||
// when this trigger is executed.
|
||||
title: '',
|
||||
description: 'Applies a range filter',
|
||||
title: i18n.translate('uiActions.triggers.selectRangeTitle', {
|
||||
defaultMessage: 'Range selection',
|
||||
}),
|
||||
description: i18n.translate('uiActions.triggers.selectRangeDescription', {
|
||||
defaultMessage: 'Select a group of values',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -17,13 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Trigger } from '.';
|
||||
|
||||
export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER';
|
||||
export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = {
|
||||
id: VALUE_CLICK_TRIGGER,
|
||||
// This is empty string to hide title of ui_actions context menu that appears
|
||||
// when this trigger is executed.
|
||||
title: '',
|
||||
description: 'Value was clicked',
|
||||
title: i18n.translate('uiActions.triggers.valueClickTitle', {
|
||||
defaultMessage: 'Single click',
|
||||
}),
|
||||
description: i18n.translate('uiActions.triggers.valueClickDescription', {
|
||||
defaultMessage: 'A single point clicked on a visualization',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -47,6 +47,7 @@ import { Vis } from '../vis';
|
|||
import { getExpressions, getUiActions } from '../services';
|
||||
import { VIS_EVENT_TO_TRIGGER } from './events';
|
||||
import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory';
|
||||
import { TriggerId } from '../../../ui_actions/public';
|
||||
|
||||
const getKeys = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<keyof T>;
|
||||
|
||||
|
@ -402,7 +403,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
});
|
||||
};
|
||||
|
||||
public supportedTriggers() {
|
||||
public supportedTriggers(): TriggerId[] {
|
||||
return this.vis.type.getSupportedTriggers?.() ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"configPath": ["ui_actions_enhanced_examples"],
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["uiActionsEnhanced", "data", "discover"],
|
||||
"requiredPlugins": ["uiActions","uiActionsEnhanced", "data", "discover"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": [
|
||||
"kibanaUtils",
|
||||
|
|
|
@ -10,6 +10,10 @@ import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/publ
|
|||
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
|
||||
import { ChartActionContext } from '../../../../../src/plugins/embeddable/public';
|
||||
import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import {
|
||||
SELECT_RANGE_TRIGGER,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export type ActionContext = ChartActionContext;
|
||||
|
||||
|
@ -19,7 +23,8 @@ export interface Config {
|
|||
|
||||
const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN';
|
||||
|
||||
export class DashboardHelloWorldDrilldown implements Drilldown<Config, ActionContext> {
|
||||
export class DashboardHelloWorldDrilldown
|
||||
implements Drilldown<Config, typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER> {
|
||||
public readonly id = SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN;
|
||||
|
||||
public readonly order = 6;
|
||||
|
@ -28,9 +33,14 @@ export class DashboardHelloWorldDrilldown implements Drilldown<Config, ActionCon
|
|||
|
||||
public readonly euiIcon = 'cheer';
|
||||
|
||||
supportedTriggers(): Array<typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER> {
|
||||
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER];
|
||||
}
|
||||
|
||||
private readonly ReactCollectConfig: React.FC<CollectConfigProps<Config>> = ({
|
||||
config,
|
||||
onConfig,
|
||||
context,
|
||||
}) => (
|
||||
<EuiFormRow label="Enter your name" fullWidth>
|
||||
<EuiFieldText
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
This folder contains a one-file example of the most basic drilldown implementation which support only RANGE_SELECT_TRIGGER.
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
|
||||
import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
|
||||
import { RangeSelectContext } from '../../../../../src/plugins/embeddable/public';
|
||||
import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { SELECT_RANGE_TRIGGER } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export interface Config {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT =
|
||||
'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT';
|
||||
|
||||
export class DashboardHelloWorldOnlyRangeSelectDrilldown
|
||||
implements Drilldown<Config, typeof SELECT_RANGE_TRIGGER> {
|
||||
public readonly id = SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT;
|
||||
|
||||
public readonly order = 7;
|
||||
|
||||
public readonly getDisplayName = () => 'Say hello only for range select';
|
||||
|
||||
public readonly euiIcon = 'cheer';
|
||||
|
||||
supportedTriggers(): Array<typeof SELECT_RANGE_TRIGGER> {
|
||||
return [SELECT_RANGE_TRIGGER];
|
||||
}
|
||||
|
||||
private readonly ReactCollectConfig: React.FC<CollectConfigProps<Config>> = ({
|
||||
config,
|
||||
onConfig,
|
||||
}) => (
|
||||
<EuiFormRow label="Enter your name" fullWidth>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
value={config.name}
|
||||
onChange={(event) => onConfig({ ...config, name: event.target.value })}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
||||
public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
|
||||
|
||||
public readonly createConfig = () => ({
|
||||
name: '',
|
||||
});
|
||||
|
||||
public readonly isConfigValid = (config: Config): config is Config => {
|
||||
return !!config.name;
|
||||
};
|
||||
|
||||
public readonly execute = async (config: Config, context: RangeSelectContext) => {
|
||||
alert(`Hello, ${config.name}, your selected range: ${JSON.stringify(context.data.range)}`);
|
||||
};
|
||||
}
|
|
@ -13,6 +13,7 @@ import { CollectConfigContainer } from './collect_config_container';
|
|||
import { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants';
|
||||
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
|
||||
import { txtGoToDiscover } from './i18n';
|
||||
import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
const isOutputWithIndexPatterns = (
|
||||
output: unknown
|
||||
|
@ -25,7 +26,8 @@ export interface Params {
|
|||
start: StartServicesGetter<Pick<Start, 'data' | 'discover'>>;
|
||||
}
|
||||
|
||||
export class DashboardToDiscoverDrilldown implements Drilldown<Config, ActionContext> {
|
||||
export class DashboardToDiscoverDrilldown
|
||||
implements Drilldown<Config, typeof APPLY_FILTER_TRIGGER> {
|
||||
constructor(protected readonly params: Params) {}
|
||||
|
||||
public readonly id = SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN;
|
||||
|
@ -36,6 +38,10 @@ export class DashboardToDiscoverDrilldown implements Drilldown<Config, ActionCon
|
|||
|
||||
public readonly euiIcon = 'discoverApp';
|
||||
|
||||
public supportedTriggers(): Array<typeof APPLY_FILTER_TRIGGER> {
|
||||
return [APPLY_FILTER_TRIGGER];
|
||||
}
|
||||
|
||||
private readonly ReactCollectConfig: React.FC<CollectConfigProps> = (props) => (
|
||||
<CollectConfigContainer {...props} params={this.params} />
|
||||
);
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ChartActionContext } from '../../../../../src/plugins/embeddable/public';
|
||||
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { ApplyGlobalFilterActionContext } from '../../../../../src/plugins/data/public';
|
||||
|
||||
export type ActionContext = ChartActionContext;
|
||||
export type ActionContext = ApplyGlobalFilterActionContext;
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
|
|
|
@ -5,11 +5,15 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiFieldText, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui';
|
||||
import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
|
||||
import { ChartActionContext } from '../../../../../src/plugins/embeddable/public';
|
||||
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import {
|
||||
SELECT_RANGE_TRIGGER,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '../../../../../src/plugins/ui_actions/public';
|
||||
import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
function isValidUrl(url: string) {
|
||||
|
@ -28,11 +32,13 @@ export interface Config {
|
|||
openInNewTab: boolean;
|
||||
}
|
||||
|
||||
export type CollectConfigProps = CollectConfigPropsBase<Config>;
|
||||
type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER;
|
||||
|
||||
export type CollectConfigProps = CollectConfigPropsBase<Config, { triggers: UrlTrigger[] }>;
|
||||
|
||||
const SAMPLE_DASHBOARD_TO_URL_DRILLDOWN = 'SAMPLE_DASHBOARD_TO_URL_DRILLDOWN';
|
||||
|
||||
export class DashboardToUrlDrilldown implements Drilldown<Config, ActionContext> {
|
||||
export class DashboardToUrlDrilldown implements Drilldown<Config, UrlTrigger> {
|
||||
public readonly id = SAMPLE_DASHBOARD_TO_URL_DRILLDOWN;
|
||||
|
||||
public readonly order = 8;
|
||||
|
@ -43,7 +49,15 @@ export class DashboardToUrlDrilldown implements Drilldown<Config, ActionContext>
|
|||
|
||||
public readonly euiIcon = 'link';
|
||||
|
||||
private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({ config, onConfig }) => (
|
||||
supportedTriggers(): UrlTrigger[] {
|
||||
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER];
|
||||
}
|
||||
|
||||
private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({
|
||||
config,
|
||||
onConfig,
|
||||
context,
|
||||
}) => (
|
||||
<>
|
||||
<EuiCallOut title="Example warning!" color="warning" iconType="help">
|
||||
<p>
|
||||
|
@ -79,6 +93,11 @@ export class DashboardToUrlDrilldown implements Drilldown<Config, ActionContext>
|
|||
onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiCallOut>
|
||||
{/* just demo how can access selected triggers*/}
|
||||
<p>Will be attached to triggers: {JSON.stringify(context.triggers)}</p>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown';
|
|||
import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown';
|
||||
import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
|
||||
import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public';
|
||||
import { DashboardHelloWorldOnlyRangeSelectDrilldown } from './dashboard_hello_world_only_range_select_drilldown';
|
||||
|
||||
export interface SetupDependencies {
|
||||
data: DataPublicPluginSetup;
|
||||
|
@ -37,6 +38,7 @@ export class UiActionsEnhancedExamplesPlugin
|
|||
const start = createStartServicesGetter(core.getStartServices);
|
||||
|
||||
uiActions.registerDrilldown(new DashboardHelloWorldDrilldown());
|
||||
uiActions.registerDrilldown(new DashboardHelloWorldOnlyRangeSelectDrilldown());
|
||||
uiActions.registerDrilldown(new DashboardToUrlDrilldown());
|
||||
uiActions.registerDrilldown(new DashboardToDiscoverDrilldown({ start }));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
APPLY_FILTER_TRIGGER,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
TriggerId,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
/**
|
||||
* We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER
|
||||
* This function appends APPLY_FILTER_TRIGGER to list of triggers if VALUE_CLICK_TRIGGER or SELECT_RANGE_TRIGGER
|
||||
*
|
||||
* TODO: this probably should be part of uiActions infrastructure,
|
||||
* but dynamic implementation of nested trigger doesn't allow to statically express such relations
|
||||
*
|
||||
* @param triggers
|
||||
*/
|
||||
export function ensureNestedTriggers(triggers: TriggerId[]): TriggerId[] {
|
||||
if (
|
||||
!triggers.includes(APPLY_FILTER_TRIGGER) &&
|
||||
(triggers.includes(VALUE_CLICK_TRIGGER) || triggers.includes(SELECT_RANGE_TRIGGER))
|
||||
) {
|
||||
return [...triggers, APPLY_FILTER_TRIGGER];
|
||||
}
|
||||
|
||||
return triggers;
|
||||
}
|
|
@ -10,9 +10,13 @@ import {
|
|||
} from './flyout_create_drilldown';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { TriggerContextMapping } from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
TriggerContextMapping,
|
||||
TriggerId,
|
||||
} from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers';
|
||||
import { uiActionsEnhancedPluginMock } from '../../../../../../ui_actions_enhanced/public/mocks';
|
||||
import { UiActionsEnhancedActionFactory } from '../../../../../../ui_actions_enhanced/public/';
|
||||
|
||||
const overlays = coreMock.createStart().overlays;
|
||||
const uiActionsEnhanced = uiActionsEnhancedPluginMock.createStartContract();
|
||||
|
@ -50,6 +54,7 @@ interface CompatibilityParams {
|
|||
isValueClickTriggerSupported?: boolean;
|
||||
isEmbeddableEnhanced?: boolean;
|
||||
rootType?: string;
|
||||
actionFactoriesTriggers?: TriggerId[];
|
||||
}
|
||||
|
||||
describe('isCompatible', () => {
|
||||
|
@ -61,9 +66,16 @@ describe('isCompatible', () => {
|
|||
isValueClickTriggerSupported = true,
|
||||
isEmbeddableEnhanced = true,
|
||||
rootType = 'dashboard',
|
||||
actionFactoriesTriggers = ['VALUE_CLICK_TRIGGER'],
|
||||
}: CompatibilityParams,
|
||||
expectedResult: boolean = true
|
||||
): Promise<void> {
|
||||
uiActionsEnhanced.getActionFactories.mockImplementation(() => [
|
||||
({
|
||||
supportedTriggers: () => actionFactoriesTriggers,
|
||||
} as unknown) as UiActionsEnhancedActionFactory,
|
||||
]);
|
||||
|
||||
let embeddable = new MockEmbeddable(
|
||||
{ id: '', viewMode: isEdit ? ViewMode.EDIT : ViewMode.VIEW },
|
||||
{
|
||||
|
@ -116,6 +128,15 @@ describe('isCompatible', () => {
|
|||
rootType: 'visualization',
|
||||
});
|
||||
});
|
||||
|
||||
test('not compatible if no triggers intersection', async () => {
|
||||
await assertNonCompatibility({
|
||||
actionFactoriesTriggers: [],
|
||||
});
|
||||
await assertNonCompatibility({
|
||||
actionFactoriesTriggers: ['SELECT_RANGE_TRIGGER'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
|
|
|
@ -6,17 +6,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ActionByType,
|
||||
APPLY_FILTER_TRIGGER,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public';
|
||||
import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { StartDependencies } from '../../../../plugin';
|
||||
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { ensureNestedTriggers } from '../drilldown_shared';
|
||||
|
||||
export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN';
|
||||
|
||||
|
@ -47,8 +43,18 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
|
|||
if (!supportedTriggers || !supportedTriggers.length) return false;
|
||||
if (context.embeddable.getRoot().type !== 'dashboard') return false;
|
||||
|
||||
return supportedTriggers.some((trigger) =>
|
||||
[VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, APPLY_FILTER_TRIGGER].includes(trigger)
|
||||
/**
|
||||
* Check if there is an intersection between all registered drilldowns possible triggers that they could be attached to
|
||||
* and triggers that current embeddable supports
|
||||
*/
|
||||
const allPossibleTriggers = this.params
|
||||
.start()
|
||||
.plugins.uiActionsEnhanced.getActionFactories()
|
||||
.map((factory) => factory.supportedTriggers())
|
||||
.reduce((res, next) => res.concat(next), []);
|
||||
|
||||
return ensureNestedTriggers(supportedTriggers).some((trigger) =>
|
||||
allPossibleTriggers.includes(trigger)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -73,6 +79,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
|
|||
onClose={() => handle.close()}
|
||||
viewMode={'create'}
|
||||
dynamicActionManager={embeddable.enhancements.dynamicActions}
|
||||
supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())}
|
||||
/>
|
||||
),
|
||||
{
|
||||
|
|
|
@ -22,6 +22,9 @@ uiActionsPlugin.setup.registerDrilldown({
|
|||
isConfigValid: () => true,
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test',
|
||||
supportedTriggers() {
|
||||
return ['VALUE_CLICK_TRIGGER'];
|
||||
},
|
||||
});
|
||||
|
||||
const actionParams: FlyoutEditDrilldownParams = {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { MenuItem } from './menu_item';
|
|||
import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public';
|
||||
import { StartDependencies } from '../../../../plugin';
|
||||
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { ensureNestedTriggers } from '../drilldown_shared';
|
||||
|
||||
export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN';
|
||||
|
||||
|
@ -62,6 +63,7 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
|
|||
onClose={() => handle.close()}
|
||||
viewMode={'manage'}
|
||||
dynamicActionManager={embeddable.enhancements.dynamicActions}
|
||||
supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())}
|
||||
/>
|
||||
),
|
||||
{
|
||||
|
|
|
@ -11,7 +11,7 @@ import { SimpleSavedObject } from '../../../../../../../../src/core/public';
|
|||
import { DashboardDrilldownConfig } from './dashboard_drilldown_config';
|
||||
import { txtDestinationDashboardNotFound } from './i18n';
|
||||
import { CollectConfigProps } from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { Config } from '../types';
|
||||
import { Config, FactoryContext } from '../types';
|
||||
import { Params } from '../drilldown';
|
||||
|
||||
const mergeDashboards = (
|
||||
|
@ -34,7 +34,7 @@ const dashboardSavedObjectToMenuItem = (
|
|||
label: savedObject.attributes.title,
|
||||
});
|
||||
|
||||
interface DashboardDrilldownCollectConfigProps extends CollectConfigProps<Config> {
|
||||
interface DashboardDrilldownCollectConfigProps extends CollectConfigProps<Config, FactoryContext> {
|
||||
params: Params;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
DashboardUrlGenerator,
|
||||
DashboardUrlGeneratorState,
|
||||
|
@ -23,7 +24,7 @@ import {
|
|||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { StartDependencies } from '../../../plugin';
|
||||
import { Config } from './types';
|
||||
import { Config, FactoryContext } from './types';
|
||||
|
||||
export interface Params {
|
||||
start: StartServicesGetter<Pick<StartDependencies, 'data' | 'uiActionsEnhanced'>>;
|
||||
|
@ -31,7 +32,7 @@ export interface Params {
|
|||
}
|
||||
|
||||
export class DashboardToDashboardDrilldown
|
||||
implements Drilldown<Config, ApplyGlobalFilterActionContext> {
|
||||
implements Drilldown<Config, typeof APPLY_FILTER_TRIGGER, FactoryContext> {
|
||||
constructor(protected readonly params: Params) {}
|
||||
|
||||
public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN;
|
||||
|
@ -59,6 +60,10 @@ export class DashboardToDashboardDrilldown
|
|||
return true;
|
||||
};
|
||||
|
||||
public supportedTriggers(): Array<typeof APPLY_FILTER_TRIGGER> {
|
||||
return [APPLY_FILTER_TRIGGER];
|
||||
}
|
||||
|
||||
public readonly getHref = async (
|
||||
config: Config,
|
||||
context: ApplyGlobalFilterActionContext
|
||||
|
|
|
@ -4,8 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public';
|
||||
import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export interface Config {
|
||||
dashboardId?: string;
|
||||
useCurrentFilters: boolean;
|
||||
useCurrentDateRange: boolean;
|
||||
}
|
||||
|
||||
export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext<typeof APPLY_FILTER_TRIGGER>;
|
||||
|
|
|
@ -14,13 +14,21 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
EuiFormFieldset,
|
||||
EuiCheckableCard,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { txtChangeButton } from './i18n';
|
||||
import { txtChangeButton, txtTriggerPickerHelpText, txtTriggerPickerLabel } from './i18n';
|
||||
import './action_wizard.scss';
|
||||
import { ActionFactory } from '../../dynamic_actions';
|
||||
import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions';
|
||||
import { Trigger, TriggerId } from '../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export interface ActionWizardProps {
|
||||
export interface ActionWizardProps<
|
||||
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
> {
|
||||
/**
|
||||
* List of available action factories
|
||||
*/
|
||||
|
@ -51,7 +59,22 @@ export interface ActionWizardProps {
|
|||
/**
|
||||
* Context will be passed into ActionFactory's methods
|
||||
*/
|
||||
context: object;
|
||||
context: ActionFactoryContext;
|
||||
|
||||
/**
|
||||
* Trigger selection has changed
|
||||
* @param triggers
|
||||
*/
|
||||
onSelectedTriggersChange: (triggers?: TriggerId[]) => void;
|
||||
|
||||
getTriggerInfo: (triggerId: TriggerId) => Trigger;
|
||||
|
||||
/**
|
||||
* List of possible triggers in current context
|
||||
*/
|
||||
supportedTriggers: TriggerId[];
|
||||
|
||||
triggerPickerDocsLink?: string;
|
||||
}
|
||||
|
||||
export const ActionWizard: React.FC<ActionWizardProps> = ({
|
||||
|
@ -61,6 +84,10 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
onConfigChange,
|
||||
config,
|
||||
context,
|
||||
onSelectedTriggersChange,
|
||||
getTriggerInfo,
|
||||
supportedTriggers,
|
||||
triggerPickerDocsLink,
|
||||
}) => {
|
||||
// auto pick action factory if there is only 1 available
|
||||
if (
|
||||
|
@ -71,7 +98,16 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
onActionFactoryChange(actionFactories[0]);
|
||||
}
|
||||
|
||||
// auto pick selected trigger if none is picked
|
||||
if (currentActionFactory && !((context.triggers?.length ?? 0) > 0)) {
|
||||
const triggers = getTriggersForActionFactory(currentActionFactory, supportedTriggers);
|
||||
if (triggers.length > 0) {
|
||||
onSelectedTriggersChange([triggers[0]]);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentActionFactory && config) {
|
||||
const allTriggers = getTriggersForActionFactory(currentActionFactory, supportedTriggers);
|
||||
return (
|
||||
<SelectedActionFactory
|
||||
actionFactory={currentActionFactory}
|
||||
|
@ -84,6 +120,10 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
onConfigChange={(newConfig) => {
|
||||
onConfigChange(newConfig);
|
||||
}}
|
||||
allTriggers={allTriggers}
|
||||
getTriggerInfo={getTriggerInfo}
|
||||
onSelectedTriggersChange={onSelectedTriggersChange}
|
||||
triggerPickerDocsLink={triggerPickerDocsLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -99,13 +139,84 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
interface SelectedActionFactoryProps {
|
||||
interface TriggerPickerProps {
|
||||
triggers: TriggerId[];
|
||||
selectedTriggers?: TriggerId[];
|
||||
getTriggerInfo: (triggerId: TriggerId) => Trigger;
|
||||
onSelectedTriggersChange: (triggers?: TriggerId[]) => void;
|
||||
triggerPickerDocsLink?: string;
|
||||
}
|
||||
|
||||
const TriggerPicker: React.FC<TriggerPickerProps> = ({
|
||||
triggers,
|
||||
selectedTriggers,
|
||||
getTriggerInfo,
|
||||
onSelectedTriggersChange,
|
||||
triggerPickerDocsLink,
|
||||
}) => {
|
||||
const selectedTrigger = selectedTriggers ? selectedTriggers[0] : undefined;
|
||||
return (
|
||||
<EuiFormFieldset
|
||||
legend={{
|
||||
children: (
|
||||
<EuiText size="s">
|
||||
<h5>
|
||||
<span>{txtTriggerPickerLabel}</span>{' '}
|
||||
<EuiLink href={triggerPickerDocsLink} target={'blank'} external>
|
||||
{txtTriggerPickerHelpText}
|
||||
</EuiLink>
|
||||
</h5>
|
||||
</EuiText>
|
||||
),
|
||||
}}
|
||||
style={{ maxWidth: `80%` }}
|
||||
>
|
||||
{triggers.map((trigger) => (
|
||||
<React.Fragment key={trigger}>
|
||||
<EuiCheckableCard
|
||||
id={trigger}
|
||||
label={
|
||||
<>
|
||||
<EuiTitle size={'xxs'}>
|
||||
<span>{getTriggerInfo(trigger)?.title ?? 'Unknown'}</span>
|
||||
</EuiTitle>
|
||||
{getTriggerInfo(trigger)?.description && (
|
||||
<div>
|
||||
<EuiText size={'s'}>
|
||||
<EuiTextColor color={'subdued'}>
|
||||
{getTriggerInfo(trigger)?.description}
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
name={trigger}
|
||||
value={trigger}
|
||||
checked={selectedTrigger === trigger}
|
||||
onChange={() => onSelectedTriggersChange([trigger])}
|
||||
data-test-subj={`triggerPicker-${trigger}`}
|
||||
/>
|
||||
<EuiSpacer size={'s'} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</EuiFormFieldset>
|
||||
);
|
||||
};
|
||||
|
||||
interface SelectedActionFactoryProps<
|
||||
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
> {
|
||||
actionFactory: ActionFactory;
|
||||
config: object;
|
||||
context: object;
|
||||
context: ActionFactoryContext;
|
||||
onConfigChange: (config: object) => void;
|
||||
showDeselect: boolean;
|
||||
onDeselect: () => void;
|
||||
allTriggers: TriggerId[];
|
||||
getTriggerInfo: (triggerId: TriggerId) => Trigger;
|
||||
onSelectedTriggersChange: (triggers?: TriggerId[]) => void;
|
||||
triggerPickerDocsLink?: string;
|
||||
}
|
||||
|
||||
export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selectedActionFactory';
|
||||
|
@ -117,6 +228,10 @@ const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({
|
|||
onConfigChange,
|
||||
config,
|
||||
context,
|
||||
allTriggers,
|
||||
getTriggerInfo,
|
||||
onSelectedTriggersChange,
|
||||
triggerPickerDocsLink,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
|
@ -144,7 +259,19 @@ const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({
|
|||
)}
|
||||
</EuiFlexGroup>
|
||||
</header>
|
||||
<EuiSpacer size="m" />
|
||||
{allTriggers.length > 1 && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<TriggerPicker
|
||||
triggers={allTriggers}
|
||||
getTriggerInfo={getTriggerInfo}
|
||||
selectedTriggers={context.triggers}
|
||||
onSelectedTriggersChange={onSelectedTriggersChange}
|
||||
triggerPickerDocsLink={triggerPickerDocsLink}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="l" />
|
||||
<div>
|
||||
<actionFactory.ReactCollectConfig
|
||||
config={config}
|
||||
|
@ -156,9 +283,11 @@ const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
interface ActionFactorySelectorProps {
|
||||
interface ActionFactorySelectorProps<
|
||||
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
> {
|
||||
actionFactories: ActionFactory[];
|
||||
context: object;
|
||||
context: ActionFactoryContext;
|
||||
onActionFactorySelected: (actionFactory: ActionFactory) => void;
|
||||
}
|
||||
|
||||
|
@ -224,3 +353,10 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
function getTriggersForActionFactory(
|
||||
actionFactory: ActionFactory,
|
||||
allTriggers: TriggerId[]
|
||||
): TriggerId[] {
|
||||
return actionFactory.supportedTriggers().filter((trigger) => allTriggers.includes(trigger));
|
||||
}
|
||||
|
|
|
@ -12,3 +12,17 @@ export const txtChangeButton = i18n.translate(
|
|||
defaultMessage: 'Change',
|
||||
}
|
||||
);
|
||||
|
||||
export const txtTriggerPickerLabel = i18n.translate(
|
||||
'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel',
|
||||
{
|
||||
defaultMessage: 'Pick a trigger:',
|
||||
}
|
||||
);
|
||||
|
||||
export const txtTriggerPickerHelpText = i18n.translate(
|
||||
'xpack.uiActionsEnhanced.components.actionWizard.helpText',
|
||||
{
|
||||
defaultMessage: "What's this?",
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,9 +8,16 @@ import React, { useState } from 'react';
|
|||
import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui';
|
||||
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { ActionWizard } from './action_wizard';
|
||||
import { ActionFactoryDefinition, ActionFactory } from '../../dynamic_actions';
|
||||
import { ActionFactory, ActionFactoryDefinition } from '../../dynamic_actions';
|
||||
import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
import { licenseMock } from '../../../../licensing/common/licensing.mock';
|
||||
import {
|
||||
APPLY_FILTER_TRIGGER,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
Trigger,
|
||||
TriggerId,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
} from '../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
type ActionBaseConfig = object;
|
||||
|
||||
|
@ -104,6 +111,9 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition<
|
|||
execute: async () => alert('Navigate to dashboard!'),
|
||||
enhancements: {},
|
||||
}),
|
||||
supportedTriggers(): any[] {
|
||||
return [APPLY_FILTER_TRIGGER];
|
||||
},
|
||||
};
|
||||
|
||||
export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () =>
|
||||
|
@ -161,16 +171,45 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition<UrlDrilldownConf
|
|||
return Promise.resolve(true);
|
||||
},
|
||||
create: () => null as any,
|
||||
supportedTriggers(): any[] {
|
||||
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER];
|
||||
},
|
||||
};
|
||||
|
||||
export const urlFactory = new ActionFactory(urlDrilldownActionFactory, () =>
|
||||
licenseMock.createLicense()
|
||||
);
|
||||
|
||||
export const mockSupportedTriggers: TriggerId[] = [
|
||||
VALUE_CLICK_TRIGGER,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
APPLY_FILTER_TRIGGER,
|
||||
];
|
||||
export const mockGetTriggerInfo = (triggerId: TriggerId): Trigger => {
|
||||
const titleMap = {
|
||||
[VALUE_CLICK_TRIGGER]: 'Single click',
|
||||
[SELECT_RANGE_TRIGGER]: 'Range selection',
|
||||
[APPLY_FILTER_TRIGGER]: 'Apply filter',
|
||||
} as Record<any, string>;
|
||||
|
||||
const descriptionMap = {
|
||||
[VALUE_CLICK_TRIGGER]: 'A single point clicked on a visualization',
|
||||
[SELECT_RANGE_TRIGGER]: 'Select a group of values',
|
||||
[APPLY_FILTER_TRIGGER]: 'Apply filter description...',
|
||||
} as Record<any, string>;
|
||||
|
||||
return {
|
||||
id: triggerId,
|
||||
title: titleMap[triggerId] ?? 'Unknown',
|
||||
description: descriptionMap[triggerId] ?? 'Unknown description',
|
||||
};
|
||||
};
|
||||
|
||||
export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory<any>> }) {
|
||||
const [state, setState] = useState<{
|
||||
currentActionFactory?: ActionFactory;
|
||||
config?: ActionBaseConfig;
|
||||
selectedTriggers?: TriggerId[];
|
||||
}>({});
|
||||
|
||||
function changeActionFactory(newActionFactory?: ActionFactory) {
|
||||
|
@ -200,7 +239,15 @@ export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory
|
|||
changeActionFactory(newActionFactory);
|
||||
}}
|
||||
currentActionFactory={state.currentActionFactory}
|
||||
context={{}}
|
||||
context={{ triggers: state.selectedTriggers ?? [] }}
|
||||
onSelectedTriggersChange={(triggers) => {
|
||||
setState({
|
||||
...state,
|
||||
selectedTriggers: triggers,
|
||||
});
|
||||
}}
|
||||
getTriggerInfo={mockGetTriggerInfo}
|
||||
supportedTriggers={[VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER]}
|
||||
/>
|
||||
<div style={{ marginTop: '44px' }} />
|
||||
<hr />
|
||||
|
@ -210,6 +257,7 @@ export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory
|
|||
Is config valid:{' '}
|
||||
{JSON.stringify(state.currentActionFactory?.isConfigValid(state.config!) ?? false)}
|
||||
</div>
|
||||
<div>Picked trigger: {state.selectedTriggers?.[0]}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,10 +25,25 @@ const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({
|
|||
alert(JSON.stringify(args));
|
||||
},
|
||||
} as any,
|
||||
getTrigger: (triggerId) => ({
|
||||
id: triggerId,
|
||||
}),
|
||||
});
|
||||
|
||||
storiesOf('components/FlyoutManageDrilldowns', module).add('default', () => (
|
||||
<EuiFlyout onClose={() => {}}>
|
||||
<FlyoutManageDrilldowns dynamicActionManager={mockDynamicActionManager} />
|
||||
</EuiFlyout>
|
||||
));
|
||||
storiesOf('components/FlyoutManageDrilldowns', module)
|
||||
.add('default (3 triggers)', () => (
|
||||
<EuiFlyout onClose={() => {}}>
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={['VALUE_CLICK_TRIGGER', 'SELECT_RANGE_TRIGGER', 'FILTER_TRIGGER']}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
))
|
||||
.add('Only filter is supported', () => (
|
||||
<EuiFlyout onClose={() => {}}>
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={['FILTER_TRIGGER']}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
));
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
import React from 'react';
|
||||
import { cleanup, fireEvent, render, wait } from '@testing-library/react/pure';
|
||||
import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns';
|
||||
import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data';
|
||||
import {
|
||||
dashboardFactory,
|
||||
mockGetTriggerInfo,
|
||||
mockSupportedTriggers,
|
||||
urlFactory,
|
||||
} from '../../../components/action_wizard/test_data';
|
||||
import { StubBrowserStorage } from '../../../../../../../src/test_utils/public/stub_browser_storage';
|
||||
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { mockDynamicActionManager } from './test_data';
|
||||
|
@ -24,6 +29,7 @@ const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({
|
|||
actionFactories: [dashboardFactory as ActionFactory, urlFactory as ActionFactory],
|
||||
storage: new Storage(new StubBrowserStorage()),
|
||||
toastService: toasts,
|
||||
getTrigger: mockGetTriggerInfo,
|
||||
});
|
||||
|
||||
// https://github.com/elastic/kibana/issues/59469
|
||||
|
@ -31,12 +37,18 @@ afterEach(cleanup);
|
|||
|
||||
beforeEach(() => {
|
||||
storage.clear();
|
||||
mockDynamicActionManager.state.set({ ...mockDynamicActionManager.state.get(), events: [] });
|
||||
(toasts as jest.Mocked<NotificationsStart['toasts']>).addSuccess.mockClear();
|
||||
(toasts as jest.Mocked<NotificationsStart['toasts']>).addError.mockClear();
|
||||
});
|
||||
|
||||
test('Allows to manage drilldowns', async () => {
|
||||
const screen = render(<FlyoutManageDrilldowns dynamicActionManager={mockDynamicActionManager} />);
|
||||
const screen = render(
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
/>
|
||||
);
|
||||
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
|
||||
|
@ -103,7 +115,12 @@ test('Allows to manage drilldowns', async () => {
|
|||
});
|
||||
|
||||
test('Can delete multiple drilldowns', async () => {
|
||||
const screen = render(<FlyoutManageDrilldowns dynamicActionManager={mockDynamicActionManager} />);
|
||||
const screen = render(
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
/>
|
||||
);
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
|
||||
|
||||
|
@ -143,6 +160,7 @@ test('Create only mode', async () => {
|
|||
dynamicActionManager={mockDynamicActionManager}
|
||||
viewMode={'create'}
|
||||
onClose={onClose}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
/>
|
||||
);
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
|
@ -163,7 +181,11 @@ test('Create only mode', async () => {
|
|||
|
||||
test('After switching between action factories state is restored', async () => {
|
||||
const screen = render(
|
||||
<FlyoutManageDrilldowns dynamicActionManager={mockDynamicActionManager} viewMode={'create'} />
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
viewMode={'create'}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
/>
|
||||
);
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0));
|
||||
|
@ -200,7 +222,12 @@ test("Error when can't save drilldown changes", async () => {
|
|||
jest.spyOn(mockDynamicActionManager, 'createEvent').mockImplementationOnce(async () => {
|
||||
throw error;
|
||||
});
|
||||
const screen = render(<FlyoutManageDrilldowns dynamicActionManager={mockDynamicActionManager} />);
|
||||
const screen = render(
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
/>
|
||||
);
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
|
||||
fireEvent.click(screen.getByText(/Create new/i));
|
||||
|
@ -218,7 +245,12 @@ test("Error when can't save drilldown changes", async () => {
|
|||
});
|
||||
|
||||
test('Should show drilldown welcome message. Should be able to dismiss it', async () => {
|
||||
let screen = render(<FlyoutManageDrilldowns dynamicActionManager={mockDynamicActionManager} />);
|
||||
let screen = render(
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
/>
|
||||
);
|
||||
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
|
||||
|
@ -228,8 +260,63 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn
|
|||
expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull();
|
||||
cleanup();
|
||||
|
||||
screen = render(<FlyoutManageDrilldowns dynamicActionManager={mockDynamicActionManager} />);
|
||||
screen = render(
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
/>
|
||||
);
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible());
|
||||
expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull();
|
||||
});
|
||||
|
||||
test('Drilldown type is not shown if no supported trigger', async () => {
|
||||
const screen = render(
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={['VALUE_CLICK_TRIGGER']}
|
||||
viewMode={'create'}
|
||||
/>
|
||||
);
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0));
|
||||
expect(screen.queryByText(/Go to Dashboard/i)).not.toBeInTheDocument(); // dashboard action is not visible, because APPLY_FILTER_TRIGGER not supported
|
||||
expect(screen.getByTestId('selectedActionFactory-Url')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Can pick a trigger', async () => {
|
||||
const screen = render(
|
||||
<FlyoutManageDrilldowns
|
||||
dynamicActionManager={mockDynamicActionManager}
|
||||
supportedTriggers={mockSupportedTriggers}
|
||||
viewMode={'create'}
|
||||
/>
|
||||
);
|
||||
// wait for initial render. It is async because resolving compatible action factories is async
|
||||
await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0));
|
||||
|
||||
// input drilldown name
|
||||
const name = 'Test name';
|
||||
fireEvent.change(screen.getByLabelText(/name/i), {
|
||||
target: { value: name },
|
||||
});
|
||||
|
||||
// select URL one
|
||||
fireEvent.click(screen.getByText(/Go to URL/i));
|
||||
|
||||
// Input url
|
||||
const URL = 'https://elastic.co';
|
||||
fireEvent.change(screen.getByLabelText(/url/i), {
|
||||
target: { value: URL },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('triggerPicker-SELECT_RANGE_TRIGGER').querySelector('input')!);
|
||||
|
||||
const [, createButton] = screen.getAllByText(/Create Drilldown/i);
|
||||
|
||||
expect(createButton).toBeEnabled();
|
||||
fireEvent.click(createButton);
|
||||
await wait(() => expect(toasts.addSuccess).toBeCalled());
|
||||
expect(mockDynamicActionManager.state.get().events[0].triggers).toEqual(['SELECT_RANGE_TRIGGER']);
|
||||
});
|
||||
|
|
|
@ -4,16 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { ToastsStart } from 'kibana/public';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import intersection from 'lodash/intersection';
|
||||
import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard';
|
||||
import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns';
|
||||
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import {
|
||||
TriggerContextMapping,
|
||||
APPLY_FILTER_TRIGGER,
|
||||
} from '../../../../../../../src/plugins/ui_actions/public';
|
||||
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { DrilldownListItem } from '../list_manage_drilldowns';
|
||||
import {
|
||||
|
@ -27,15 +25,29 @@ import {
|
|||
} from './i18n';
|
||||
import {
|
||||
ActionFactory,
|
||||
BaseActionFactoryContext,
|
||||
DynamicActionManager,
|
||||
SerializedAction,
|
||||
SerializedEvent,
|
||||
} from '../../../dynamic_actions';
|
||||
import { ExtraActionFactoryContext } from '../types';
|
||||
|
||||
interface ConnectedFlyoutManageDrilldownsProps {
|
||||
interface ConnectedFlyoutManageDrilldownsProps<
|
||||
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
> {
|
||||
dynamicActionManager: DynamicActionManager;
|
||||
viewMode?: 'create' | 'manage';
|
||||
onClose?: () => void;
|
||||
|
||||
/**
|
||||
* List of possible triggers in current context
|
||||
*/
|
||||
supportedTriggers: TriggerId[];
|
||||
|
||||
/**
|
||||
* Extra action factory context passed into action factories CollectConfig, getIconType, getDisplayName and etc...
|
||||
*/
|
||||
extraContext?: ExtraActionFactoryContext<ActionFactoryContext>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,8 +64,10 @@ export function createFlyoutManageDrilldowns({
|
|||
storage,
|
||||
toastService,
|
||||
docsLink,
|
||||
getTrigger,
|
||||
}: {
|
||||
actionFactories: ActionFactory[];
|
||||
getTrigger: (triggerId: TriggerId) => Trigger;
|
||||
storage: IStorageWrapper;
|
||||
toastService: ToastsStart;
|
||||
docsLink?: string;
|
||||
|
@ -66,19 +80,10 @@ export function createFlyoutManageDrilldowns({
|
|||
return (props: ConnectedFlyoutManageDrilldownsProps) => {
|
||||
const isCreateOnly = props.viewMode === 'create';
|
||||
|
||||
// TODO: https://github.com/elastic/kibana/issues/59569
|
||||
const selectedTriggers: Array<keyof TriggerContextMapping> = React.useMemo(
|
||||
() => [APPLY_FILTER_TRIGGER],
|
||||
[]
|
||||
const factoryContext: BaseActionFactoryContext = useMemo(
|
||||
() => ({ ...props.extraContext, triggers: props.supportedTriggers }),
|
||||
[props.extraContext, props.supportedTriggers]
|
||||
);
|
||||
|
||||
const factoryContext: object = React.useMemo(
|
||||
() => ({
|
||||
triggers: selectedTriggers,
|
||||
}),
|
||||
[selectedTriggers]
|
||||
);
|
||||
|
||||
const actionFactories = useCompatibleActionFactoriesForCurrentContext(
|
||||
allActionFactories,
|
||||
factoryContext
|
||||
|
@ -122,6 +127,7 @@ export function createFlyoutManageDrilldowns({
|
|||
actionFactory: allActionFactoriesById[drilldownToEdit.action.factoryId],
|
||||
actionConfig: drilldownToEdit.action.config as object,
|
||||
name: drilldownToEdit.action.name,
|
||||
selectedTriggers: (drilldownToEdit.triggers ?? []) as TriggerId[],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -130,16 +136,22 @@ export function createFlyoutManageDrilldowns({
|
|||
*/
|
||||
function mapToDrilldownToDrilldownListItem(drilldown: SerializedEvent): DrilldownListItem {
|
||||
const actionFactory = allActionFactoriesById[drilldown.action.factoryId];
|
||||
const drilldownFactoryContext: BaseActionFactoryContext = {
|
||||
...props.extraContext,
|
||||
triggers: drilldown.triggers as TriggerId[],
|
||||
};
|
||||
return {
|
||||
id: drilldown.eventId,
|
||||
drilldownName: drilldown.action.name,
|
||||
actionName: actionFactory?.getDisplayName(factoryContext) ?? drilldown.action.factoryId,
|
||||
icon: actionFactory?.getIconType(factoryContext),
|
||||
actionName:
|
||||
actionFactory?.getDisplayName(drilldownFactoryContext) ?? drilldown.action.factoryId,
|
||||
icon: actionFactory?.getIconType(drilldownFactoryContext),
|
||||
error: !actionFactory
|
||||
? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development
|
||||
: !actionFactory.isCompatibleLicence()
|
||||
? insufficientLicenseLevel
|
||||
: undefined,
|
||||
triggers: drilldown.triggers.map((trigger) => getTrigger(trigger as TriggerId)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -155,7 +167,7 @@ export function createFlyoutManageDrilldowns({
|
|||
onClose={props.onClose}
|
||||
mode={route === Routes.Create ? 'create' : 'edit'}
|
||||
onBack={isCreateOnly ? undefined : () => setRoute(Routes.Manage)}
|
||||
onSubmit={({ actionConfig, actionFactory, name }) => {
|
||||
onSubmit={({ actionConfig, actionFactory, name, selectedTriggers }) => {
|
||||
if (route === Routes.Create) {
|
||||
createDrilldown(
|
||||
{
|
||||
|
@ -192,13 +204,23 @@ export function createFlyoutManageDrilldowns({
|
|||
setRoute(Routes.Manage);
|
||||
setCurrentEditId(null);
|
||||
}}
|
||||
actionFactoryContext={factoryContext}
|
||||
extraActionFactoryContext={props.extraContext}
|
||||
initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()}
|
||||
supportedTriggers={props.supportedTriggers}
|
||||
getTrigger={getTrigger}
|
||||
/>
|
||||
);
|
||||
|
||||
case Routes.Manage:
|
||||
default:
|
||||
// show trigger column in case if there is more then 1 possible trigger in current context
|
||||
const showTriggerColumn =
|
||||
intersection(
|
||||
props.supportedTriggers,
|
||||
actionFactories
|
||||
.map((factory) => factory.supportedTriggers())
|
||||
.reduce((res, next) => res.concat(next), [])
|
||||
).length > 1;
|
||||
return (
|
||||
<FlyoutListManageDrilldowns
|
||||
docsLink={docsLink}
|
||||
|
@ -218,16 +240,16 @@ export function createFlyoutManageDrilldowns({
|
|||
setRoute(Routes.Create);
|
||||
}}
|
||||
onClose={props.onClose}
|
||||
showTriggerColumn={showTriggerColumn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useCompatibleActionFactoriesForCurrentContext<Context extends object = object>(
|
||||
actionFactories: ActionFactory[],
|
||||
context: Context
|
||||
) {
|
||||
function useCompatibleActionFactoriesForCurrentContext<
|
||||
Context extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
>(actionFactories: ActionFactory[], context: Context) {
|
||||
const [compatibleActionFactories, setCompatibleActionFactories] = useState<ActionFactory[]>();
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
@ -236,13 +258,18 @@ function useCompatibleActionFactoriesForCurrentContext<Context extends object =
|
|||
actionFactories.map((factory) => factory.isCompatible(context))
|
||||
);
|
||||
if (canceled) return;
|
||||
setCompatibleActionFactories(actionFactories.filter((_, i) => compatibility[i]));
|
||||
|
||||
const compatibleFactories = actionFactories.filter((_, i) => compatibility[i]);
|
||||
const triggerSupportedFactories = compatibleFactories.filter((factory) =>
|
||||
factory.supportedTriggers().some((trigger) => context.triggers.includes(trigger))
|
||||
);
|
||||
setCompatibleActionFactories(triggerSupportedFactories);
|
||||
}
|
||||
updateCompatibleFactoriesForContext();
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [context, actionFactories]);
|
||||
}, [context, actionFactories, context.triggers]);
|
||||
|
||||
return compatibleActionFactories;
|
||||
}
|
||||
|
@ -280,10 +307,7 @@ function useDrilldownsStateManager(actionManager: DynamicActionManager, toastSer
|
|||
}
|
||||
}
|
||||
|
||||
async function createDrilldown(
|
||||
action: SerializedAction,
|
||||
selectedTriggers: Array<keyof TriggerContextMapping>
|
||||
) {
|
||||
async function createDrilldown(action: SerializedAction, selectedTriggers: TriggerId[]) {
|
||||
await run(async () => {
|
||||
await actionManager.createEvent(action, selectedTriggers);
|
||||
toastService.addSuccess({
|
||||
|
@ -296,7 +320,7 @@ function useDrilldownsStateManager(actionManager: DynamicActionManager, toastSer
|
|||
async function editDrilldown(
|
||||
drilldownId: string,
|
||||
action: SerializedAction,
|
||||
selectedTriggers: Array<keyof TriggerContextMapping>
|
||||
selectedTriggers: TriggerId[]
|
||||
) {
|
||||
await run(async () => {
|
||||
await actionManager.updateEvent(drilldownId, action, selectedTriggers);
|
||||
|
|
|
@ -10,12 +10,24 @@ import { storiesOf } from '@storybook/react';
|
|||
import { FlyoutDrilldownWizard } from './index';
|
||||
import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data';
|
||||
import { ActionFactory } from '../../../dynamic_actions';
|
||||
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
const otherProps = {
|
||||
supportedTriggers: [
|
||||
'VALUE_CLICK_TRIGGER',
|
||||
'SELECT_RANGE_TRIGGER',
|
||||
'FILTER_TRIGGER',
|
||||
] as TriggerId[],
|
||||
onClose: () => {},
|
||||
getTrigger: (id: TriggerId) => ({ id } as Trigger),
|
||||
};
|
||||
|
||||
storiesOf('components/FlyoutDrilldownWizard', module)
|
||||
.add('default', () => {
|
||||
return (
|
||||
<FlyoutDrilldownWizard
|
||||
drilldownActionFactories={[urlFactory as ActionFactory, dashboardFactory as ActionFactory]}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -23,11 +35,11 @@ storiesOf('components/FlyoutDrilldownWizard', module)
|
|||
return (
|
||||
<EuiFlyout onClose={() => {}}>
|
||||
<FlyoutDrilldownWizard
|
||||
onClose={() => {}}
|
||||
drilldownActionFactories={[
|
||||
urlFactory as ActionFactory,
|
||||
dashboardFactory as ActionFactory,
|
||||
]}
|
||||
{...otherProps}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
@ -36,7 +48,6 @@ storiesOf('components/FlyoutDrilldownWizard', module)
|
|||
return (
|
||||
<EuiFlyout onClose={() => {}}>
|
||||
<FlyoutDrilldownWizard
|
||||
onClose={() => {}}
|
||||
drilldownActionFactories={[
|
||||
urlFactory as ActionFactory,
|
||||
dashboardFactory as ActionFactory,
|
||||
|
@ -50,6 +61,7 @@ storiesOf('components/FlyoutDrilldownWizard', module)
|
|||
},
|
||||
}}
|
||||
mode={'edit'}
|
||||
{...otherProps}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
@ -58,7 +70,6 @@ storiesOf('components/FlyoutDrilldownWizard', module)
|
|||
return (
|
||||
<EuiFlyout onClose={() => {}}>
|
||||
<FlyoutDrilldownWizard
|
||||
onClose={() => {}}
|
||||
drilldownActionFactories={[dashboardFactory as ActionFactory]}
|
||||
initialDrilldownWizardConfig={{
|
||||
name: 'My fancy drilldown',
|
||||
|
@ -69,6 +80,7 @@ storiesOf('components/FlyoutDrilldownWizard', module)
|
|||
},
|
||||
}}
|
||||
mode={'edit'}
|
||||
{...otherProps}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { FormDrilldownWizard } from '../form_drilldown_wizard';
|
||||
import { FlyoutFrame } from '../flyout_frame';
|
||||
|
@ -16,15 +16,21 @@ import {
|
|||
txtEditDrilldownTitle,
|
||||
} from './i18n';
|
||||
import { DrilldownHelloBar } from '../drilldown_hello_bar';
|
||||
import { ActionFactory } from '../../../dynamic_actions';
|
||||
import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions';
|
||||
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
import { ExtraActionFactoryContext } from '../types';
|
||||
|
||||
export interface DrilldownWizardConfig<ActionConfig extends object = object> {
|
||||
name: string;
|
||||
actionFactory?: ActionFactory;
|
||||
actionConfig?: ActionConfig;
|
||||
selectedTriggers?: TriggerId[];
|
||||
}
|
||||
|
||||
export interface FlyoutDrilldownWizardProps<CurrentActionConfig extends object = object> {
|
||||
export interface FlyoutDrilldownWizardProps<
|
||||
CurrentActionConfig extends object = object,
|
||||
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
> {
|
||||
drilldownActionFactories: ActionFactory[];
|
||||
|
||||
onSubmit?: (drilldownWizardConfig: Required<DrilldownWizardConfig>) => void;
|
||||
|
@ -38,9 +44,16 @@ export interface FlyoutDrilldownWizardProps<CurrentActionConfig extends object =
|
|||
showWelcomeMessage?: boolean;
|
||||
onWelcomeHideClick?: () => void;
|
||||
|
||||
actionFactoryContext?: object;
|
||||
extraActionFactoryContext?: ExtraActionFactoryContext<ActionFactoryContext>;
|
||||
|
||||
docsLink?: string;
|
||||
|
||||
getTrigger: (triggerId: TriggerId) => Trigger;
|
||||
|
||||
/**
|
||||
* List of possible triggers in current context
|
||||
*/
|
||||
supportedTriggers: TriggerId[];
|
||||
}
|
||||
|
||||
function useWizardConfigState(
|
||||
|
@ -51,6 +64,7 @@ function useWizardConfigState(
|
|||
setName: (name: string) => void;
|
||||
setActionConfig: (actionConfig: object) => void;
|
||||
setActionFactory: (actionFactory?: ActionFactory) => void;
|
||||
setSelectedTriggers: (triggers?: TriggerId[]) => void;
|
||||
}
|
||||
] {
|
||||
const [wizardConfig, setWizardConfig] = useState<DrilldownWizardConfig>(
|
||||
|
@ -105,6 +119,12 @@ function useWizardConfigState(
|
|||
});
|
||||
}
|
||||
},
|
||||
setSelectedTriggers: (selectedTriggers: TriggerId[] = []) => {
|
||||
setWizardConfig({
|
||||
...wizardConfig,
|
||||
selectedTriggers,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -119,12 +139,15 @@ export function FlyoutDrilldownWizard<CurrentActionConfig extends object = objec
|
|||
showWelcomeMessage = true,
|
||||
onWelcomeHideClick,
|
||||
drilldownActionFactories,
|
||||
actionFactoryContext,
|
||||
extraActionFactoryContext,
|
||||
docsLink,
|
||||
getTrigger,
|
||||
supportedTriggers,
|
||||
}: FlyoutDrilldownWizardProps<CurrentActionConfig>) {
|
||||
const [wizardConfig, { setActionFactory, setActionConfig, setName }] = useWizardConfigState(
|
||||
initialDrilldownWizardConfig
|
||||
);
|
||||
const [
|
||||
wizardConfig,
|
||||
{ setActionFactory, setActionConfig, setName, setSelectedTriggers },
|
||||
] = useWizardConfigState(initialDrilldownWizardConfig);
|
||||
|
||||
const isActionValid = (
|
||||
config: DrilldownWizardConfig
|
||||
|
@ -132,10 +155,19 @@ export function FlyoutDrilldownWizard<CurrentActionConfig extends object = objec
|
|||
if (!wizardConfig.name) return false;
|
||||
if (!wizardConfig.actionFactory) return false;
|
||||
if (!wizardConfig.actionConfig) return false;
|
||||
if (!wizardConfig.selectedTriggers || wizardConfig.selectedTriggers.length === 0) return false;
|
||||
|
||||
return wizardConfig.actionFactory.isConfigValid(wizardConfig.actionConfig);
|
||||
};
|
||||
|
||||
const actionFactoryContext: BaseActionFactoryContext = useMemo(
|
||||
() => ({
|
||||
...extraActionFactoryContext,
|
||||
triggers: wizardConfig.selectedTriggers ?? [],
|
||||
}),
|
||||
[extraActionFactoryContext, wizardConfig.selectedTriggers]
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
|
@ -171,7 +203,11 @@ export function FlyoutDrilldownWizard<CurrentActionConfig extends object = objec
|
|||
currentActionFactory={wizardConfig.actionFactory}
|
||||
onActionFactoryChange={setActionFactory}
|
||||
actionFactories={drilldownActionFactories}
|
||||
actionFactoryContext={actionFactoryContext!}
|
||||
actionFactoryContext={actionFactoryContext}
|
||||
onSelectedTriggersChange={setSelectedTriggers}
|
||||
supportedTriggers={supportedTriggers}
|
||||
getTriggerInfo={getTrigger}
|
||||
triggerPickerDocsLink={docsLink}
|
||||
/>
|
||||
{mode === 'edit' && (
|
||||
<>
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface FlyoutListManageDrilldownsProps {
|
|||
onDelete?: (drilldownIds: string[]) => void;
|
||||
showWelcomeMessage?: boolean;
|
||||
onWelcomeHideClick?: () => void;
|
||||
showTriggerColumn?: boolean;
|
||||
}
|
||||
|
||||
export function FlyoutListManageDrilldowns({
|
||||
|
@ -30,6 +31,7 @@ export function FlyoutListManageDrilldowns({
|
|||
onEdit,
|
||||
showWelcomeMessage = true,
|
||||
onWelcomeHideClick,
|
||||
showTriggerColumn,
|
||||
}: FlyoutListManageDrilldownsProps) {
|
||||
return (
|
||||
<FlyoutFrame
|
||||
|
@ -46,6 +48,7 @@ export function FlyoutListManageDrilldowns({
|
|||
onCreate={onCreate}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
showTriggerColumn={showTriggerColumn}
|
||||
/>
|
||||
</FlyoutFrame>
|
||||
);
|
||||
|
|
|
@ -7,13 +7,25 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { FormDrilldownWizard } from './index';
|
||||
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
const otherProps = {
|
||||
supportedTriggers: [
|
||||
'VALUE_CLICK_TRIGGER',
|
||||
'SELECT_RANGE_TRIGGER',
|
||||
'FILTER_TRIGGER',
|
||||
] as TriggerId[],
|
||||
getTriggerInfo: (id: TriggerId) => ({ id } as Trigger),
|
||||
onSelectedTriggersChange: () => {},
|
||||
actionFactoryContext: { triggers: [] as TriggerId[] },
|
||||
};
|
||||
|
||||
const DemoEditName: React.FC = () => {
|
||||
const [name, setName] = React.useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormDrilldownWizard name={name} onNameChange={setName} actionFactoryContext={{}} />{' '}
|
||||
<FormDrilldownWizard name={name} onNameChange={setName} {...otherProps} />{' '}
|
||||
<div>name: {name}</div>
|
||||
</>
|
||||
);
|
||||
|
@ -21,9 +33,9 @@ const DemoEditName: React.FC = () => {
|
|||
|
||||
storiesOf('components/FormDrilldownWizard', module)
|
||||
.add('default', () => {
|
||||
return <FormDrilldownWizard actionFactoryContext={{}} />;
|
||||
return <FormDrilldownWizard {...otherProps} />;
|
||||
})
|
||||
.add('[name=foobar]', () => {
|
||||
return <FormDrilldownWizard name={'foobar'} actionFactoryContext={{}} />;
|
||||
return <FormDrilldownWizard name={'foobar'} {...otherProps} />;
|
||||
})
|
||||
.add('can edit name', () => <DemoEditName />);
|
||||
|
|
|
@ -9,20 +9,32 @@ import { render } from 'react-dom';
|
|||
import { FormDrilldownWizard } from './form_drilldown_wizard';
|
||||
import { render as renderTestingLibrary, fireEvent, cleanup } from '@testing-library/react/pure';
|
||||
import { txtNameOfDrilldown } from './i18n';
|
||||
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const otherProps = {
|
||||
actionFactoryContext: { triggers: [] as TriggerId[] },
|
||||
supportedTriggers: [
|
||||
'VALUE_CLICK_TRIGGER',
|
||||
'SELECT_RANGE_TRIGGER',
|
||||
'FILTER_TRIGGER',
|
||||
] as TriggerId[],
|
||||
getTriggerInfo: (id: TriggerId) => ({ id } as Trigger),
|
||||
onSelectedTriggersChange: () => {},
|
||||
};
|
||||
|
||||
describe('<FormDrilldownWizard>', () => {
|
||||
test('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
render(<FormDrilldownWizard onNameChange={() => {}} actionFactoryContext={{}} />, div);
|
||||
render(<FormDrilldownWizard onNameChange={() => {}} {...otherProps} />, div);
|
||||
});
|
||||
|
||||
describe('[name=]', () => {
|
||||
test('if name not provided, uses to empty string', () => {
|
||||
const div = document.createElement('div');
|
||||
|
||||
render(<FormDrilldownWizard actionFactoryContext={{}} />, div);
|
||||
render(<FormDrilldownWizard {...otherProps} />, div);
|
||||
|
||||
const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement;
|
||||
|
||||
|
@ -32,13 +44,13 @@ describe('<FormDrilldownWizard>', () => {
|
|||
test('can set initial name input field value', () => {
|
||||
const div = document.createElement('div');
|
||||
|
||||
render(<FormDrilldownWizard name={'foo'} actionFactoryContext={{}} />, div);
|
||||
render(<FormDrilldownWizard name={'foo'} {...otherProps} />, div);
|
||||
|
||||
const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement;
|
||||
|
||||
expect(input?.value).toBe('foo');
|
||||
|
||||
render(<FormDrilldownWizard name={'bar'} actionFactoryContext={{}} />, div);
|
||||
render(<FormDrilldownWizard name={'bar'} {...otherProps} />, div);
|
||||
|
||||
expect(input?.value).toBe('bar');
|
||||
});
|
||||
|
@ -46,7 +58,7 @@ describe('<FormDrilldownWizard>', () => {
|
|||
test('fires onNameChange callback on name change', () => {
|
||||
const onNameChange = jest.fn();
|
||||
const utils = renderTestingLibrary(
|
||||
<FormDrilldownWizard name={''} onNameChange={onNameChange} actionFactoryContext={{}} />
|
||||
<FormDrilldownWizard name={''} onNameChange={onNameChange} {...otherProps} />
|
||||
);
|
||||
const input = utils.getByLabelText(txtNameOfDrilldown);
|
||||
|
||||
|
|
|
@ -8,25 +8,43 @@ import React from 'react';
|
|||
import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n';
|
||||
import { ActionFactory } from '../../../dynamic_actions';
|
||||
import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions';
|
||||
import { ActionWizard } from '../../../components/action_wizard';
|
||||
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions';
|
||||
|
||||
const noopFn = () => {};
|
||||
|
||||
export interface FormDrilldownWizardProps {
|
||||
export interface FormDrilldownWizardProps<
|
||||
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
> {
|
||||
name?: string;
|
||||
onNameChange?: (name: string) => void;
|
||||
|
||||
currentActionFactory?: ActionFactory;
|
||||
onActionFactoryChange?: (actionFactory?: ActionFactory) => void;
|
||||
actionFactoryContext: object;
|
||||
actionFactoryContext: ActionFactoryContext;
|
||||
|
||||
actionConfig?: object;
|
||||
onActionConfigChange?: (config: object) => void;
|
||||
|
||||
actionFactories?: ActionFactory[];
|
||||
|
||||
/**
|
||||
* Trigger selection has changed
|
||||
* @param triggers
|
||||
*/
|
||||
onSelectedTriggersChange: (triggers?: TriggerId[]) => void;
|
||||
|
||||
getTriggerInfo: (triggerId: TriggerId) => Trigger;
|
||||
|
||||
/**
|
||||
* List of possible triggers in current context
|
||||
*/
|
||||
supportedTriggers: TriggerId[];
|
||||
|
||||
triggerPickerDocsLink?: string;
|
||||
}
|
||||
|
||||
export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
|
||||
|
@ -38,6 +56,10 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
|
|||
onActionFactoryChange = noopFn,
|
||||
actionFactories = [],
|
||||
actionFactoryContext,
|
||||
onSelectedTriggersChange,
|
||||
getTriggerInfo,
|
||||
supportedTriggers,
|
||||
triggerPickerDocsLink,
|
||||
}) => {
|
||||
const nameFragment = (
|
||||
<EuiFormRow label={txtNameOfDrilldown}>
|
||||
|
@ -86,6 +108,10 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
|
|||
onActionFactoryChange={(actionFactory) => onActionFactoryChange(actionFactory)}
|
||||
onConfigChange={(config) => onActionConfigChange(config)}
|
||||
context={actionFactoryContext}
|
||||
onSelectedTriggersChange={onSelectedTriggersChange}
|
||||
getTriggerInfo={getTriggerInfo}
|
||||
supportedTriggers={supportedTriggers}
|
||||
triggerPickerDocsLink={triggerPickerDocsLink}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -11,9 +11,26 @@ import { ListManageDrilldowns } from './list_manage_drilldowns';
|
|||
storiesOf('components/ListManageDrilldowns', module).add('default', () => (
|
||||
<ListManageDrilldowns
|
||||
drilldowns={[
|
||||
{ id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1', icon: 'dashboardApp' },
|
||||
{ id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2', icon: 'dashboardApp' },
|
||||
{ id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3' },
|
||||
{
|
||||
id: '1',
|
||||
actionName: 'Dashboard',
|
||||
drilldownName: 'Drilldown 1',
|
||||
icon: 'dashboardApp',
|
||||
triggers: [{ title: 'trigger' }],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
actionName: 'Dashboard',
|
||||
drilldownName: 'Drilldown 2',
|
||||
icon: 'dashboardApp',
|
||||
triggers: [{ title: 'trigger' }],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
actionName: 'Dashboard',
|
||||
drilldownName: 'Drilldown 3',
|
||||
triggers: [{ title: 'trigger', description: 'trigger' }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -30,6 +30,12 @@ export interface DrilldownListItem {
|
|||
drilldownName: string;
|
||||
icon?: string;
|
||||
error?: string;
|
||||
triggers?: Trigger[];
|
||||
}
|
||||
|
||||
interface Trigger {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ListManageDrilldownsProps {
|
||||
|
@ -38,6 +44,8 @@ export interface ListManageDrilldownsProps {
|
|||
onEdit?: (id: string) => void;
|
||||
onCreate?: () => void;
|
||||
onDelete?: (ids: string[]) => void;
|
||||
|
||||
showTriggerColumn?: boolean;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
@ -49,14 +57,13 @@ export function ListManageDrilldowns({
|
|||
onEdit = noop,
|
||||
onCreate = noop,
|
||||
onDelete = noop,
|
||||
showTriggerColumn = true,
|
||||
}: ListManageDrilldownsProps) {
|
||||
const [selectedDrilldowns, setSelectedDrilldowns] = useState<string[]>([]);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<DrilldownListItem>> = [
|
||||
{
|
||||
name: 'Name',
|
||||
truncateText: true,
|
||||
width: '50%',
|
||||
'data-test-subj': 'drilldownListItemName',
|
||||
render: (drilldown: DrilldownListItem) => (
|
||||
<div>
|
||||
|
@ -85,21 +92,38 @@ export function ListManageDrilldowns({
|
|||
<EuiIcon type={drilldown.icon} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false} className="eui-textTruncate">
|
||||
<EuiFlexItem grow={false} style={{ flexWrap: 'wrap' }}>
|
||||
<EuiTextColor color="subdued">{drilldown.actionName}</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
showTriggerColumn && {
|
||||
name: 'Trigger',
|
||||
textOnly: true,
|
||||
render: (drilldown: DrilldownListItem) =>
|
||||
drilldown.triggers?.map((trigger, idx) =>
|
||||
trigger.description ? (
|
||||
<EuiToolTip content={trigger.description} key={idx}>
|
||||
<EuiTextColor color="subdued">{trigger.title ?? 'unknown'}</EuiTextColor>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiTextColor color="subdued" key={idx}>
|
||||
{trigger.title ?? 'unknown'}
|
||||
</EuiTextColor>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
width: '64px',
|
||||
render: (drilldown: DrilldownListItem) => (
|
||||
<EuiButtonEmpty size="xs" onClick={() => onEdit(drilldown.id)}>
|
||||
{txtEditDrilldown}
|
||||
</EuiButtonEmpty>
|
||||
),
|
||||
},
|
||||
];
|
||||
].filter(Boolean) as Array<EuiBasicTableColumn<DrilldownListItem>>;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { BaseActionFactoryContext } from '../../dynamic_actions';
|
||||
|
||||
/**
|
||||
* Interface used as piece of ActionFactoryContext that is passed in from drilldown wizard component to action factories
|
||||
* Omitted values are added inside the wizard and then full {@link BaseActionFactoryContext} passed into action factory methods
|
||||
*/
|
||||
export type ExtraActionFactoryContext<
|
||||
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
|
||||
> = Omit<ActionFactoryContext, 'triggers'>;
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionFactoryDefinition } from '../dynamic_actions';
|
||||
import { ActionFactoryDefinition, BaseActionFactoryContext } from '../dynamic_actions';
|
||||
import { LicenseType } from '../../../licensing/public';
|
||||
import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public';
|
||||
import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
/**
|
||||
|
@ -21,9 +22,14 @@ import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/pu
|
|||
* and provided to the `execute` function of the drilldown. This object contains
|
||||
* information about the action user performed.
|
||||
*/
|
||||
|
||||
export interface DrilldownDefinition<
|
||||
Config extends object = object,
|
||||
ExecutionContext extends object = object
|
||||
SupportedTriggers extends TriggerId = TriggerId,
|
||||
FactoryContext extends BaseActionFactoryContext<SupportedTriggers> = {
|
||||
triggers: SupportedTriggers[];
|
||||
},
|
||||
ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
|
||||
> {
|
||||
/**
|
||||
* Globally unique identifier for this drilldown.
|
||||
|
@ -45,7 +51,12 @@ export interface DrilldownDefinition<
|
|||
/**
|
||||
* Function that returns default config for this drilldown.
|
||||
*/
|
||||
createConfig: ActionFactoryDefinition<Config, object, ExecutionContext>['createConfig'];
|
||||
createConfig: ActionFactoryDefinition<
|
||||
Config,
|
||||
SupportedTriggers,
|
||||
FactoryContext,
|
||||
ExecutionContext
|
||||
>['createConfig'];
|
||||
|
||||
/**
|
||||
* `UiComponent` that collections config for this drilldown. You can create
|
||||
|
@ -66,13 +77,23 @@ export interface DrilldownDefinition<
|
|||
* export const CollectConfig = uiToReactComponent(ReactCollectConfig);
|
||||
* ```
|
||||
*/
|
||||
CollectConfig: ActionFactoryDefinition<Config, object, ExecutionContext>['CollectConfig'];
|
||||
CollectConfig: ActionFactoryDefinition<
|
||||
Config,
|
||||
SupportedTriggers,
|
||||
FactoryContext,
|
||||
ExecutionContext
|
||||
>['CollectConfig'];
|
||||
|
||||
/**
|
||||
* A validator function for the config object. Should always return a boolean
|
||||
* given any input.
|
||||
*/
|
||||
isConfigValid: ActionFactoryDefinition<Config, object, ExecutionContext>['isConfigValid'];
|
||||
isConfigValid: ActionFactoryDefinition<
|
||||
Config,
|
||||
SupportedTriggers,
|
||||
FactoryContext,
|
||||
ExecutionContext
|
||||
>['isConfigValid'];
|
||||
|
||||
/**
|
||||
* Name of EUI icon to display when showing this drilldown to user.
|
||||
|
@ -106,4 +127,10 @@ export interface DrilldownDefinition<
|
|||
config: Config,
|
||||
context: ExecutionContext | ActionExecutionContext<ExecutionContext>
|
||||
): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* List of triggers supported by this drilldown type
|
||||
* This is used in trigger picker when configuring drilldown
|
||||
*/
|
||||
supportedTriggers(): SupportedTriggers[];
|
||||
}
|
||||
|
|
|
@ -19,12 +19,13 @@ const def: ActionFactoryDefinition = {
|
|||
getDisplayName: () => name,
|
||||
enhancements: {},
|
||||
}),
|
||||
supportedTriggers: () => [],
|
||||
};
|
||||
|
||||
describe('License & ActionFactory', () => {
|
||||
test('no license requirements', async () => {
|
||||
const factory = new ActionFactory(def, () => licensingMock.createLicense());
|
||||
expect(await factory.isCompatible({})).toBe(true);
|
||||
expect(await factory.isCompatible({ triggers: [] })).toBe(true);
|
||||
expect(factory.isCompatibleLicence()).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -32,7 +33,7 @@ describe('License & ActionFactory', () => {
|
|||
const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () =>
|
||||
licensingMock.createLicense()
|
||||
);
|
||||
expect(await factory.isCompatible({})).toBe(true);
|
||||
expect(await factory.isCompatible({ triggers: [] })).toBe(true);
|
||||
expect(factory.isCompatibleLicence()).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -40,7 +41,7 @@ describe('License & ActionFactory', () => {
|
|||
const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () =>
|
||||
licensingMock.createLicense({ license: { type: 'gold' } })
|
||||
);
|
||||
expect(await factory.isCompatible({})).toBe(true);
|
||||
expect(await factory.isCompatible({ triggers: [] })).toBe(true);
|
||||
expect(factory.isCompatibleLicence()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,20 +5,32 @@
|
|||
*/
|
||||
|
||||
import { uiToReactComponent } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { UiActionsPresentable as Presentable } from '../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
TriggerContextMapping,
|
||||
TriggerId,
|
||||
UiActionsPresentable as Presentable,
|
||||
} from '../../../../../src/plugins/ui_actions/public';
|
||||
import { ActionFactoryDefinition } from './action_factory_definition';
|
||||
import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { SerializedAction } from './types';
|
||||
import { BaseActionFactoryContext, SerializedAction } from './types';
|
||||
import { ILicense } from '../../../licensing/public';
|
||||
import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export class ActionFactory<
|
||||
Config extends object = object,
|
||||
FactoryContext extends object = object,
|
||||
ActionContext extends object = object
|
||||
SupportedTriggers extends TriggerId = TriggerId,
|
||||
FactoryContext extends BaseActionFactoryContext<SupportedTriggers> = {
|
||||
triggers: SupportedTriggers[];
|
||||
},
|
||||
ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
|
||||
> implements Omit<Presentable<FactoryContext>, 'getHref'>, Configurable<Config, FactoryContext> {
|
||||
constructor(
|
||||
protected readonly def: ActionFactoryDefinition<Config, FactoryContext, ActionContext>,
|
||||
protected readonly def: ActionFactoryDefinition<
|
||||
Config,
|
||||
SupportedTriggers,
|
||||
FactoryContext,
|
||||
ActionContext
|
||||
>,
|
||||
protected readonly getLicence: () => ILicense
|
||||
) {}
|
||||
|
||||
|
@ -74,4 +86,8 @@ export class ActionFactory<
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
public supportedTriggers(): SupportedTriggers[] {
|
||||
return this.def.supportedTriggers();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
*/
|
||||
|
||||
import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { SerializedAction } from './types';
|
||||
import { BaseActionFactoryContext, SerializedAction } from './types';
|
||||
import { LicenseType } from '../../../licensing/public';
|
||||
import {
|
||||
TriggerContextMapping,
|
||||
TriggerId,
|
||||
UiActionsActionDefinition as ActionDefinition,
|
||||
UiActionsPresentable as Presentable,
|
||||
} from '../../../../../src/plugins/ui_actions/public';
|
||||
|
@ -17,8 +19,11 @@ import {
|
|||
*/
|
||||
export interface ActionFactoryDefinition<
|
||||
Config extends object = object,
|
||||
FactoryContext extends object = object,
|
||||
ActionContext extends object = object
|
||||
SupportedTriggers extends TriggerId = TriggerId,
|
||||
FactoryContext extends BaseActionFactoryContext<SupportedTriggers> = {
|
||||
triggers: SupportedTriggers[];
|
||||
},
|
||||
ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
|
||||
>
|
||||
extends Partial<Omit<Presentable<FactoryContext>, 'getHref'>>,
|
||||
Configurable<Config, FactoryContext> {
|
||||
|
@ -42,4 +47,6 @@ export interface ActionFactoryDefinition<
|
|||
create(
|
||||
serializedAction: Omit<SerializedAction<Config>, 'factoryId'>
|
||||
): ActionDefinition<ActionContext>;
|
||||
|
||||
supportedTriggers(): SupportedTriggers[];
|
||||
}
|
||||
|
|
|
@ -24,6 +24,9 @@ const actionFactoryDefinition1: ActionFactoryDefinition = {
|
|||
execute: async () => {},
|
||||
getDisplayName: () => name,
|
||||
}),
|
||||
supportedTriggers() {
|
||||
return ['VALUE_CLICK_TRIGGER'];
|
||||
},
|
||||
};
|
||||
|
||||
const actionFactoryDefinition2: ActionFactoryDefinition = {
|
||||
|
@ -36,6 +39,9 @@ const actionFactoryDefinition2: ActionFactoryDefinition = {
|
|||
execute: async () => {},
|
||||
getDisplayName: () => name,
|
||||
}),
|
||||
supportedTriggers() {
|
||||
return ['VALUE_CLICK_TRIGGER'];
|
||||
},
|
||||
};
|
||||
|
||||
const event1: SerializedEvent = {
|
||||
|
@ -417,6 +423,21 @@ describe('DynamicActionManager', () => {
|
|||
|
||||
expect(actions.size).toBe(0);
|
||||
});
|
||||
|
||||
test('throws when trigger is unknown', async () => {
|
||||
const { manager, uiActions } = setup([]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -84,7 +84,17 @@ export class DynamicActionManager {
|
|||
return actionDefinition.isCompatible(context);
|
||||
},
|
||||
});
|
||||
for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId);
|
||||
|
||||
const supportedTriggers = factory.supportedTriggers();
|
||||
for (const trigger of triggers) {
|
||||
if (!supportedTriggers.includes(trigger as any))
|
||||
throw new Error(
|
||||
`Can't attach [action=${actionId}] to [trigger=${trigger}]. Supported triggers for this action: ${supportedTriggers.join(
|
||||
','
|
||||
)}`
|
||||
);
|
||||
uiActions.attachAction(trigger as any, actionId);
|
||||
}
|
||||
}
|
||||
|
||||
protected killAction({ eventId, triggers }: SerializedEvent) {
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TriggerId } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export interface SerializedAction<Config = unknown> {
|
||||
readonly factoryId: string;
|
||||
readonly name: string;
|
||||
|
@ -18,3 +20,10 @@ export interface SerializedEvent {
|
|||
triggers: string[];
|
||||
action: SerializedAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action factory context passed into ActionFactories' CollectConfig, getDisplayName, getIconType
|
||||
*/
|
||||
export interface BaseActionFactoryContext<SupportedTriggers extends TriggerId = TriggerId> {
|
||||
triggers: SupportedTriggers[];
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export {
|
|||
DynamicActionManagerParams as UiActionsEnhancedDynamicActionManagerParams,
|
||||
DynamicActionManagerState as UiActionsEnhancedDynamicActionManagerState,
|
||||
MemoryActionStorage as UiActionsEnhancedMemoryActionStorage,
|
||||
BaseActionFactoryContext as UiActionsEnhancedBaseActionFactoryContext,
|
||||
} from './dynamic_actions';
|
||||
|
||||
export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns';
|
||||
|
|
|
@ -13,7 +13,11 @@ import {
|
|||
} from '../../../../src/core/public';
|
||||
import { createReactOverlays } from '../../../../src/plugins/kibana_react/public';
|
||||
import { UI_SETTINGS } from '../../../../src/plugins/data/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
TriggerId,
|
||||
UiActionsSetup,
|
||||
UiActionsStart,
|
||||
} from '../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
PANEL_BADGE_TRIGGER,
|
||||
|
@ -116,6 +120,7 @@ export class AdvancedUiActionsPublicPlugin
|
|||
...this.enhancements,
|
||||
FlyoutManageDrilldowns: createFlyoutManageDrilldowns({
|
||||
actionFactories: this.enhancements.getActionFactories(),
|
||||
getTrigger: (triggerId: TriggerId) => uiActions.getTrigger(triggerId),
|
||||
storage: new Storage(window?.localStorage),
|
||||
toastService: core.notifications.toasts,
|
||||
docsLink: core.docLinks.links.dashboard.drilldowns,
|
||||
|
|
|
@ -18,6 +18,9 @@ describe('UiActionsService', () => {
|
|||
createConfig: () => ({}),
|
||||
isConfigValid: () => true,
|
||||
create: () => ({} as any),
|
||||
supportedTriggers() {
|
||||
return ['VALUE_CLICK_TRIGGER'];
|
||||
},
|
||||
};
|
||||
const factoryDefinition2: ActionFactoryDefinition = {
|
||||
id: 'test-factory-2',
|
||||
|
@ -25,6 +28,9 @@ describe('UiActionsService', () => {
|
|||
createConfig: () => ({}),
|
||||
isConfigValid: () => true,
|
||||
create: () => ({} as any),
|
||||
supportedTriggers() {
|
||||
return ['VALUE_CLICK_TRIGGER'];
|
||||
},
|
||||
};
|
||||
|
||||
test('.getActionFactories() returns empty array if no action factories registered', () => {
|
||||
|
|
|
@ -5,9 +5,14 @@
|
|||
*/
|
||||
|
||||
import { ActionFactoryRegistry } from '../types';
|
||||
import { ActionFactory, ActionFactoryDefinition } from '../dynamic_actions';
|
||||
import {
|
||||
ActionFactory,
|
||||
ActionFactoryDefinition,
|
||||
BaseActionFactoryContext,
|
||||
} from '../dynamic_actions';
|
||||
import { DrilldownDefinition } from '../drilldowns';
|
||||
import { ILicense } from '../../../licensing/common/types';
|
||||
import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export interface UiActionsServiceEnhancementsParams {
|
||||
readonly actionFactories?: ActionFactoryRegistry;
|
||||
|
@ -29,19 +34,24 @@ export class UiActionsServiceEnhancements {
|
|||
*/
|
||||
public readonly registerActionFactory = <
|
||||
Config extends object = object,
|
||||
FactoryContext extends object = object,
|
||||
ActionContext extends object = object
|
||||
SupportedTriggers extends TriggerId = TriggerId,
|
||||
FactoryContext extends BaseActionFactoryContext<SupportedTriggers> = {
|
||||
triggers: SupportedTriggers[];
|
||||
},
|
||||
ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
|
||||
>(
|
||||
definition: ActionFactoryDefinition<Config, FactoryContext, ActionContext>
|
||||
definition: ActionFactoryDefinition<Config, SupportedTriggers, FactoryContext, ActionContext>
|
||||
) => {
|
||||
if (this.actionFactories.has(definition.id)) {
|
||||
throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`);
|
||||
}
|
||||
|
||||
const actionFactory = new ActionFactory<Config, FactoryContext, ActionContext>(
|
||||
definition,
|
||||
this.getLicenseInfo
|
||||
);
|
||||
const actionFactory = new ActionFactory<
|
||||
Config,
|
||||
SupportedTriggers,
|
||||
FactoryContext,
|
||||
ActionContext
|
||||
>(definition, this.getLicenseInfo);
|
||||
|
||||
this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory<any, any, any>);
|
||||
};
|
||||
|
@ -68,7 +78,11 @@ export class UiActionsServiceEnhancements {
|
|||
*/
|
||||
public readonly registerDrilldown = <
|
||||
Config extends object = object,
|
||||
ExecutionContext extends object = object
|
||||
SupportedTriggers extends TriggerId = TriggerId,
|
||||
FactoryContext extends BaseActionFactoryContext<SupportedTriggers> = {
|
||||
triggers: SupportedTriggers[];
|
||||
},
|
||||
ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
|
||||
>({
|
||||
id: factoryId,
|
||||
order,
|
||||
|
@ -80,8 +94,14 @@ export class UiActionsServiceEnhancements {
|
|||
execute,
|
||||
getHref,
|
||||
minimalLicense,
|
||||
}: DrilldownDefinition<Config, ExecutionContext>): void => {
|
||||
const actionFactory: ActionFactoryDefinition<Config, object, ExecutionContext> = {
|
||||
supportedTriggers,
|
||||
}: DrilldownDefinition<Config, SupportedTriggers, FactoryContext, ExecutionContext>): void => {
|
||||
const actionFactory: ActionFactoryDefinition<
|
||||
Config,
|
||||
SupportedTriggers,
|
||||
FactoryContext,
|
||||
ExecutionContext
|
||||
> = {
|
||||
id: factoryId,
|
||||
minimalLicense,
|
||||
order,
|
||||
|
@ -89,6 +109,7 @@ export class UiActionsServiceEnhancements {
|
|||
createConfig,
|
||||
isConfigValid,
|
||||
getDisplayName,
|
||||
supportedTriggers,
|
||||
getIconType: () => euiIcon,
|
||||
isCompatible: async () => true,
|
||||
create: (serializedAction) => ({
|
||||
|
@ -99,7 +120,7 @@ export class UiActionsServiceEnhancements {
|
|||
execute: async (context) => await execute(serializedAction.config, context),
|
||||
getHref: getHref ? async (context) => getHref(serializedAction.config, context) : undefined,
|
||||
}),
|
||||
} as ActionFactoryDefinition<Config, object, ExecutionContext>;
|
||||
} as ActionFactoryDefinition<Config, SupportedTriggers, FactoryContext, ExecutionContext>;
|
||||
|
||||
this.registerActionFactory(actionFactory);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue