Context menu trigger for URL Drilldown (#81158)

* feat: 🎸 add context menu trigger to URL drilldown

* fix: 🐛 translate "Drilldowns" grouping title

* feat: 🎸 add dynamic action grouping to dynamic actions

* fix: 🐛 add translations to trigger texts

* feat: 🎸 enambe ctx menu trigger in both flyouts, move to end

* fix: 🐛 show context menu event scope variable sfor ctx menu

* test: 💍 add tests

* fix: 🐛 use correct namespace for translation keys

* docs: ✏️ update autogenerated docs

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Vadim Dalecky 2020-10-30 13:04:48 +01:00 committed by GitHub
parent 21615c16ef
commit aaadbe88c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 187 additions and 60 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) &gt; [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md)
## isContextMenuTriggerContext variable
<b>Signature:</b>
```typescript
isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext
```

View file

@ -77,6 +77,7 @@
| [contextMenuTrigger](./kibana-plugin-plugins-embeddable-public.contextmenutrigger.md) | |
| [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | |
| [EmbeddableRenderer](./kibana-plugin-plugins-embeddable-public.embeddablerenderer.md) | Helper react component to render an embeddable Can be used if you have an embeddable object or an embeddable factory Supports updating input by passing <code>input</code> prop |
| [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) | |
| [isRangeSelectTriggerContext](./kibana-plugin-plugins-embeddable-public.israngeselecttriggercontext.md) | |
| [isValueClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isvalueclicktriggercontext.md) | |
| [PANEL\_BADGE\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_badge_trigger.md) | |

View file

@ -70,6 +70,7 @@ export {
isSavedObjectEmbeddableInput,
isRangeSelectTriggerContext,
isValueClickTriggerContext,
isContextMenuTriggerContext,
EmbeddableStateTransfer,
EmbeddableEditorState,
EmbeddablePackageState,

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { Datatable } from '../../../../expressions';
import { Trigger } from '../../../../ui_actions/public';
import { IEmbeddable } from '..';
@ -53,6 +54,39 @@ export type ChartActionContext<T extends IEmbeddable = IEmbeddable> =
| ValueClickContext<T>
| RangeSelectContext<T>;
export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER';
export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = {
id: CONTEXT_MENU_TRIGGER,
title: i18n.translate('embeddableApi.contextMenuTrigger.title', {
defaultMessage: 'Context menu',
}),
description: i18n.translate('embeddableApi.contextMenuTrigger.description', {
defaultMessage: 'A panel top-right corner context menu click.',
}),
};
export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER';
export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = {
id: PANEL_BADGE_TRIGGER,
title: i18n.translate('embeddableApi.panelBadgeTrigger.title', {
defaultMessage: 'Panel badges',
}),
description: i18n.translate('embeddableApi.panelBadgeTrigger.description', {
defaultMessage: 'Actions appear in title bar when an embeddable loads in a panel.',
}),
};
export const PANEL_NOTIFICATION_TRIGGER = 'PANEL_NOTIFICATION_TRIGGER';
export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = {
id: PANEL_NOTIFICATION_TRIGGER,
title: i18n.translate('embeddableApi.panelNotificationTrigger.title', {
defaultMessage: 'Panel notifications',
}),
description: i18n.translate('embeddableApi.panelNotificationTrigger.description', {
defaultMessage: 'Actions appear in top-right corner of a panel.',
}),
};
export const isValueClickTriggerContext = (
context: ChartActionContext
): context is ValueClickContext => context.data && 'data' in context.data;
@ -61,23 +95,8 @@ export const isRangeSelectTriggerContext = (
context: ChartActionContext
): context is RangeSelectContext => context.data && 'range' in context.data;
export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER';
export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = {
id: CONTEXT_MENU_TRIGGER,
title: 'Context menu',
description: 'Triggered on top-right corner context-menu select.',
};
export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER';
export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = {
id: PANEL_BADGE_TRIGGER,
title: 'Panel badges',
description: 'Actions appear in title bar when an embeddable loads in a panel.',
};
export const PANEL_NOTIFICATION_TRIGGER = 'PANEL_NOTIFICATION_TRIGGER';
export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = {
id: PANEL_NOTIFICATION_TRIGGER,
title: 'Panel notifications',
description: 'Actions appear in top-right corner of a panel.',
};
export const isContextMenuTriggerContext = (context: unknown): context is EmbeddableContext =>
!!context &&
typeof context === 'object' &&
!!(context as EmbeddableContext).embeddable &&
typeof (context as EmbeddableContext).embeddable === 'object';

View file

@ -695,6 +695,11 @@ export interface IEmbeddable<I extends EmbeddableInput = EmbeddableInput, O exte
updateInput(changes: Partial<I>): void;
}
// Warning: (ae-missing-release-tag) "isContextMenuTriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext;
// Warning: (ae-missing-release-tag) "isErrorEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -884,7 +889,7 @@ export const withEmbeddableSubscription: <I extends EmbeddableInput, O extends E
// src/plugins/embeddable/common/types.ts:59:3 - (ae-forgotten-export) The symbol "TimeRange" needs to be exported by the entry point index.d.ts
// src/plugins/embeddable/common/types.ts:64:3 - (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point index.d.ts
// src/plugins/embeddable/common/types.ts:69:3 - (ae-forgotten-export) The symbol "Filter" needs to be exported by the entry point index.d.ts
// src/plugins/embeddable/public/lib/triggers/triggers.ts:45:5 - (ae-forgotten-export) The symbol "Datatable" needs to be exported by the entry point index.d.ts
// src/plugins/embeddable/public/lib/triggers/triggers.ts:46:5 - (ae-forgotten-export) The symbol "Datatable" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

@ -12,8 +12,9 @@ import {
} 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
* We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER.
* This function appends APPLY_FILTER_TRIGGER to the list of triggers if either VALUE_CLICK_TRIGGER
* or SELECT_RANGE_TRIGGER was executed.
*
* TODO: this probably should be part of uiActions infrastructure,
* but dynamic implementation of nested trigger doesn't allow to statically express such relations

View file

@ -129,7 +129,7 @@ describe('isCompatible', () => {
});
});
test('not compatible if no triggers intersection', async () => {
test('not compatible if no triggers intersect', async () => {
await assertNonCompatibility({
actionFactoriesTriggers: [],
});

View file

@ -10,9 +10,12 @@ import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/pub
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
import {
isEnhancedEmbeddable,
embeddableEnhancedContextMenuDrilldownGrouping,
embeddableEnhancedDrilldownGrouping,
} from '../../../../../../embeddable_enhanced/public';
import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public';
import {
CONTEXT_MENU_TRIGGER,
EmbeddableContext,
} from '../../../../../../../../src/plugins/embeddable/public';
import { StartDependencies } from '../../../../plugin';
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
import { ensureNestedTriggers } from '../drilldown_shared';
@ -27,7 +30,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN;
public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN;
public order = 12;
public grouping = embeddableEnhancedContextMenuDrilldownGrouping;
public grouping = embeddableEnhancedDrilldownGrouping;
constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {}
@ -83,7 +86,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
onClose={() => handle.close()}
viewMode={'create'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
triggers={ensureNestedTriggers(embeddable.supportedTriggers())}
triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]}
placeContext={{ embeddable }}
/>
),

View file

@ -10,12 +10,16 @@ import {
reactToUiComponent,
toMountPoint,
} from '../../../../../../../../src/plugins/kibana_react/public';
import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
import {
EmbeddableContext,
ViewMode,
CONTEXT_MENU_TRIGGER,
} from '../../../../../../../../src/plugins/embeddable/public';
import { txtDisplayName } from './i18n';
import { MenuItem } from './menu_item';
import {
isEnhancedEmbeddable,
embeddableEnhancedContextMenuDrilldownGrouping,
embeddableEnhancedDrilldownGrouping,
} from '../../../../../../embeddable_enhanced/public';
import { StartDependencies } from '../../../../plugin';
import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public';
@ -31,7 +35,7 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN;
public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN;
public order = 10;
public grouping = embeddableEnhancedContextMenuDrilldownGrouping;
public grouping = embeddableEnhancedDrilldownGrouping;
constructor(protected readonly params: FlyoutEditDrilldownParams) {}
@ -67,7 +71,7 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
onClose={() => handle.close()}
viewMode={'manage'}
dynamicActionManager={embeddable.enhancements.dynamicActions}
triggers={ensureNestedTriggers(embeddable.supportedTriggers())}
triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]}
placeContext={{ embeddable }}
/>
),

View file

@ -6,7 +6,11 @@
import React from 'react';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
import {
ChartActionContext,
CONTEXT_MENU_TRIGGER,
IEmbeddable,
} from '../../../../../../src/plugins/embeddable/public';
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
import {
SELECT_RANGE_TRIGGER,
@ -34,7 +38,10 @@ interface UrlDrilldownDeps {
export type ActionContext = ChartActionContext;
export type Config = UrlDrilldownConfig;
export type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER;
export type UrlTrigger =
| typeof CONTEXT_MENU_TRIGGER
| typeof VALUE_CLICK_TRIGGER
| typeof SELECT_RANGE_TRIGGER;
export interface ActionFactoryContext extends BaseActionFactoryContext<UrlTrigger> {
embeddable?: IEmbeddable;
}
@ -58,7 +65,7 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
public readonly euiIcon = 'link';
supportedTriggers(): UrlTrigger[] {
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER];
return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, CONTEXT_MENU_TRIGGER];
}
private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({

View file

@ -87,25 +87,25 @@ describe('VALUE_CLICK_TRIGGER', () => {
]) as ValueClickTriggerEventScope;
expect(mockEventScope.points.length).toBeGreaterThan(3);
expect(mockEventScope.points).toMatchInlineSnapshot(`
Array [
Object {
"key": "event.points.0.key",
"value": "event.points.0.value",
},
Object {
"key": "event.points.1.key",
"value": "event.points.1.value",
},
Object {
"key": "event.points.2.key",
"value": "event.points.2.value",
},
Object {
"key": "event.points.3.key",
"value": "event.points.3.value",
},
]
`);
Array [
Object {
"key": "event.points.0.key",
"value": "event.points.0.value",
},
Object {
"key": "event.points.1.key",
"value": "event.points.1.value",
},
Object {
"key": "event.points.2.key",
"value": "event.points.2.value",
},
Object {
"key": "event.points.3.key",
"value": "event.points.3.value",
},
]
`);
});
});
@ -130,3 +130,12 @@ describe('VALUE_CLICK_TRIGGER', () => {
});
});
});
describe('CONTEXT_MENU_TRIGGER', () => {
test('getMockEventScope() results in empty scope', () => {
const mockEventScope = getMockEventScope([
'CONTEXT_MENU_TRIGGER',
]) as ValueClickTriggerEventScope;
expect(mockEventScope).toEqual({});
});
});

View file

@ -14,11 +14,15 @@ import {
IEmbeddable,
isRangeSelectTriggerContext,
isValueClickTriggerContext,
isContextMenuTriggerContext,
RangeSelectContext,
ValueClickContext,
} from '../../../../../../src/plugins/embeddable/public';
import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown';
import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
} from '../../../../../../src/plugins/ui_actions/public';
type ContextScopeInput = ActionContext | ActionFactoryContext;
@ -101,7 +105,10 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld
* URL drilldown event scope,
* available as {{event.$}}
*/
export type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope;
export type UrlDrilldownEventScope =
| ValueClickTriggerEventScope
| RangeSelectTriggerEventScope
| ContextMenuTriggerEventScope;
export type EventScopeInput = ActionContext;
export interface ValueClickTriggerEventScope {
key?: string;
@ -115,11 +122,15 @@ export interface RangeSelectTriggerEventScope {
to?: string | number;
}
export type ContextMenuTriggerEventScope = object;
export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope {
if (isRangeSelectTriggerContext(eventScopeInput)) {
return getEventScopeFromRangeSelectTriggerContext(eventScopeInput);
} else if (isValueClickTriggerContext(eventScopeInput)) {
return getEventScopeFromValueClickTriggerContext(eventScopeInput);
} else if (isContextMenuTriggerContext(eventScopeInput)) {
return {};
} else {
throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger");
}
@ -169,7 +180,9 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco
from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago
to: new Date().toISOString(),
};
} else {
}
if (trigger === VALUE_CLICK_TRIGGER) {
// number of mock points to generate
// should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER
const nPoints = 4;
@ -184,6 +197,8 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco
points,
};
}
return {};
}
type Primitive = string | number | boolean | null;

View file

@ -4,15 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { IEmbeddable } from '../../../../../src/plugins/embeddable/public';
import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public';
export const contextMenuDrilldownGrouping: PresentableGrouping<{
export const drilldownGrouping: PresentableGrouping<{
embeddable?: IEmbeddable;
}> = [
{
id: 'drilldowns',
getDisplayName: () => 'Drilldowns',
getDisplayName: () =>
i18n.translate('xpack.embeddableEnhanced.Drilldowns', {
defaultMessage: 'Drilldowns',
}),
getIconType: () => 'symlink',
order: 25,
},

View file

@ -20,4 +20,4 @@ export function plugin(context: PluginInitializerContext) {
export { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types';
export { isEnhancedEmbeddable } from './embeddables';
export { contextMenuDrilldownGrouping as embeddableEnhancedContextMenuDrilldownGrouping } from './actions';
export { drilldownGrouping as embeddableEnhancedDrilldownGrouping } from './actions';

View file

@ -0,0 +1,23 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { IEmbeddable } from '../../../../../src/plugins/embeddable/public';
import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public';
export const dynamicActionGrouping: PresentableGrouping<{
embeddable?: IEmbeddable;
}> = [
{
id: 'dynamicActions',
getDisplayName: () =>
i18n.translate('xpack.uiActionsEnhanced.CustomActions', {
defaultMessage: 'Custom actions',
}),
getIconType: () => 'symlink',
order: 26,
},
];

View file

@ -13,6 +13,7 @@ import { UiActionsServiceEnhancements } from '../services';
import { ActionFactoryDefinition } from './action_factory_definition';
import { SerializedAction, SerializedEvent } from './types';
import { licensingMock } from '../../../licensing/public/mocks';
import { dynamicActionGrouping } from './dynamic_action_grouping';
const actionFactoryDefinition1: ActionFactoryDefinition = {
id: 'ACTION_FACTORY_1',
@ -294,6 +295,27 @@ describe('DynamicActionManager', () => {
expect(manager.state.get().events.length).toBe(1);
});
test('adds revived actiosn to "dynamic action" grouping', async () => {
const { manager, uiActions, actions } = setup([]);
const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
};
uiActions.registerActionFactory(actionFactoryDefinition1);
await manager.start();
expect(manager.state.get().events.length).toBe(0);
await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']);
const createdAction = actions.values().next().value;
expect(createdAction.grouping).toBe(dynamicActionGrouping);
});
test('optimistically adds event to UI state', async () => {
const { manager, uiActions } = setup([]);
const action: SerializedAction = {

View file

@ -18,6 +18,7 @@ import {
} from '../../../../../src/plugins/kibana_utils/common';
import { StartContract } from '../plugin';
import { SerializedAction, SerializedEvent } from './types';
import { dynamicActionGrouping } from './dynamic_action_grouping';
const compareEvents = (
a: ReadonlyArray<{ eventId: string }>,
@ -93,6 +94,7 @@ export class DynamicActionManager {
uiActions.registerAction({
...actionDefinition,
id: actionId,
grouping: dynamicActionGrouping,
isCompatible: async (context) => {
if (!(await isCompatible(context))) return false;
if (!actionDefinition.isCompatible) return true;