Skip to content

Commit ab6fb4b

Browse files
authored
Drilldown events 5 (#59885)
* feat: ๐ŸŽธ display drilldowns in context menu only on one embed * feat: ๐ŸŽธ clear dynamic actions from registry when embed unloads * fix: ๐Ÿ› fix OSS TypeScript errors
1 parent beb053b commit ab6fb4b

File tree

10 files changed

+173
-57
lines changed

10 files changed

+173
-57
lines changed

โ€Žsrc/plugins/embeddable/public/lib/embeddables/embeddable.tsxโ€Ž

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export abstract class Embeddable<
4040
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
4141
TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput
4242
> implements IEmbeddable<TEmbeddableInput, TEmbeddableOutput> {
43+
static runtimeId: number = 0;
44+
45+
public readonly runtimeId = Embeddable.runtimeId++;
46+
4347
public readonly parent?: IContainer;
4448
public readonly isContainer: boolean = false;
4549
public abstract readonly type: string;
@@ -63,7 +67,7 @@ export abstract class Embeddable<
6367
if (!this.params.uiActions) return undefined;
6468
if (!this.__dynamicActions) {
6569
this.__dynamicActions = new UiActionsDynamicActionManager({
66-
isCompatible: async () => true,
70+
isCompatible: async ({ embeddable }: any) => embeddable.runtimeId === this.runtimeId,
6771
storage: new EmbeddableActionStorage(this),
6872
uiActions: this.params.uiActions,
6973
});
@@ -78,7 +82,6 @@ export abstract class Embeddable<
7882
parent?: IContainer,
7983
public readonly params: EmbeddableParams = {}
8084
) {
81-
window.emb = this;
8285
this.id = input.id;
8386
this.output = {
8487
title: getPanelTitle(input, output),
@@ -104,7 +107,12 @@ export abstract class Embeddable<
104107
}
105108

106109
if (this.dynamicActions) {
107-
this.dynamicActions.start();
110+
this.dynamicActions.start().catch(error => {
111+
/* eslint-disable */
112+
console.log('Failed to start embeddable dynamic actions', this);
113+
console.error(error);
114+
/* eslint-enable */
115+
});
108116
}
109117
}
110118

@@ -184,6 +192,16 @@ export abstract class Embeddable<
184192
*/
185193
public destroy(): void {
186194
this.destoyed = true;
195+
196+
if (this.dynamicActions) {
197+
this.dynamicActions.stop().catch(error => {
198+
/* eslint-disable */
199+
console.log('Failed to stop embeddable dynamic actions', this);
200+
console.error(error);
201+
/* eslint-enable */
202+
});
203+
}
204+
187205
if (this.parentSubscription) {
188206
this.parentSubscription.unsubscribe();
189207
}

โ€Žsrc/plugins/embeddable/public/lib/embeddables/i_embeddable.tsโ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ export interface IEmbeddable<
8383
**/
8484
readonly id: string;
8585

86+
/**
87+
* Unique ID an embeddable is assigned each time it is initialized. This ID
88+
* is different for different instances of the same embeddable. For example,
89+
* if the same dashboard is rendered twice on the screen, all embeddable
90+
* instances will have a unique `runtimeId`.
91+
*/
92+
readonly runtimeId?: number;
93+
8694
/**
8795
* Default implementation of dynamic action API for embeddables.
8896
*/

โ€Žsrc/plugins/ui_actions/public/actions/create_action.tsโ€Ž

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717
* under the License.
1818
*/
1919

20-
import { Ensure } from '@kbn/utility-types';
21-
import { Action } from './action';
22-
import { TriggerContextMapping } from '../types';
20+
import { ActionContextMapping } from '../types';
21+
import { ActionByType } from './action';
22+
import { ActionType } from '../types';
2323
import { ActionDefinition } from './action';
2424

25-
export function createAction<T extends keyof TriggerContextMapping>(
26-
action: ActionDefinition<Ensure<TriggerContextMapping[T], object>>
27-
): Action<TriggerContextMapping[T], T> {
25+
interface ActionDefinitionByType<T extends ActionType>
26+
extends Omit<ActionDefinition<ActionContextMapping[T]>, 'id'> {
27+
id?: string;
28+
}
29+
30+
export function createAction<T extends ActionType>(
31+
action: ActionDefinitionByType<T>
32+
): ActionByType<T> {
2833
return {
2934
getIconType: () => undefined,
3035
order: 0,
@@ -33,5 +38,5 @@ export function createAction<T extends keyof TriggerContextMapping>(
3338
getDisplayName: () => '',
3439
getHref: () => undefined,
3540
...action,
36-
} as Action<TriggerContextMapping[T], T>;
41+
} as ActionByType<T>;
3742
}

โ€Žsrc/plugins/ui_actions/public/actions/dynamic_action_manager.tsโ€Ž

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ import { ActionDefinition } from './action';
2525

2626
export interface DynamicActionManagerParams {
2727
storage: ActionStorage;
28-
uiActions: Pick<UiActionsService, 'registerAction' | 'attachAction' | 'getActionFactory'>;
28+
uiActions: Pick<
29+
UiActionsService,
30+
'addTriggerAction' | 'removeTriggerAction' | 'getActionFactory'
31+
>;
2932
isCompatible: <C = unknown>(context: C) => Promise<boolean>;
3033
}
3134

3235
export class DynamicActionManager {
3336
static idPrefixCounter = 0;
3437

35-
private readonly idPrefix = 'DYN_ACTION_' + DynamicActionManager.idPrefixCounter++;
38+
private readonly idPrefix = `D_ACTION_${DynamicActionManager.idPrefixCounter++}_`;
3639

3740
constructor(protected readonly params: DynamicActionManagerParams) {}
3841

@@ -49,15 +52,11 @@ export class DynamicActionManager {
4952
}
5053

5154
public async stop() {
52-
/*
53-
const { storage, uiActions } = this.params;
54-
const events = await storage.list();
55+
const events = await this.params.storage.list();
5556

5657
for (const event of events) {
57-
uiActions.detachAction(event.triggerId, event.action.id);
58-
uiActions.unregisterAction(event.action.id);
58+
this.killAction(event);
5959
}
60-
*/
6160
}
6261

6362
public async createEvent(action: SerializedAction<unknown>, triggerId = 'VALUE_CLICK_TRIGGER') {
@@ -71,6 +70,10 @@ export class DynamicActionManager {
7170
this.reviveAction(event);
7271
}
7372

73+
public async count(): Promise<number> {
74+
return await this.params.storage.count();
75+
}
76+
7477
protected reviveAction(event: SerializedEvent) {
7578
const { eventId, triggerId, action } = event;
7679
const { uiActions, isCompatible } = this.params;
@@ -86,8 +89,12 @@ export class DynamicActionManager {
8689
getIconType: context => factory.getIconType(context),
8790
};
8891

89-
uiActions.attachAction(triggerId as any, actionDefinition as any);
92+
uiActions.addTriggerAction(triggerId as any, actionDefinition);
9093
}
9194

92-
protected killAction(actionId: string) {}
95+
protected killAction({ eventId, triggerId }: SerializedEvent) {
96+
const { uiActions } = this.params;
97+
const actionId = this.generateActionId(eventId);
98+
uiActions.removeTriggerAction(triggerId as any, actionId);
99+
}
93100
}

โ€Žsrc/plugins/ui_actions/public/mocks.tsโ€Ž

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,22 @@ const createSetupContract = (): Setup => {
3939

4040
const createStartContract = (): Start => {
4141
const startContract: Start = {
42+
addTriggerAction: jest.fn(),
4243
attachAction: jest.fn(),
43-
registerAction: jest.fn(),
44-
registerTrigger: jest.fn(),
45-
registerActionFactory: jest.fn(),
46-
getAction: jest.fn(),
44+
clear: jest.fn(),
4745
detachAction: jest.fn(),
4846
executeTriggerActions: jest.fn(),
47+
fork: jest.fn(),
48+
getAction: jest.fn(),
4949
getActionFactories: jest.fn(),
50+
getActionFactory: jest.fn(),
5051
getTrigger: jest.fn(),
5152
getTriggerActions: jest.fn((id: TriggerId) => []),
5253
getTriggerCompatibleActions: jest.fn(),
53-
clear: jest.fn(),
54-
fork: jest.fn(),
54+
registerAction: jest.fn(),
55+
registerActionFactory: jest.fn(),
56+
registerTrigger: jest.fn(),
57+
removeTriggerAction: jest.fn(),
5558
};
5659

5760
return startContract;

โ€Žsrc/plugins/ui_actions/public/service/ui_actions_service.tsโ€Ž

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
AnyActionFactoryDefinition,
3636
ActionFactory,
3737
AnyActionFactory,
38+
ActionDefinition,
3839
} from '../actions';
3940
import { Trigger, TriggerContext } from '../triggers/trigger';
4041
import { TriggerInternal } from '../triggers/trigger_internal';
@@ -104,6 +105,48 @@ export class UiActionsService {
104105
return action;
105106
};
106107

108+
protected readonly unregisterAction = (actionId: string): void => {
109+
if (!this.actions.has(actionId)) {
110+
throw new Error(`Action [action.id = ${actionId}] is not registered.`);
111+
}
112+
113+
this.actions.delete(actionId);
114+
};
115+
116+
public readonly addTriggerAction = <TriggerId extends keyof TriggerContextMapping>(
117+
triggerId: TriggerId,
118+
definition: ActionDefinition<TriggerContextMapping[TriggerId]>
119+
) => {
120+
// Check if trigger exists, if not, next line throws.
121+
this.getTrigger(triggerId);
122+
123+
const action = this.registerAction(definition);
124+
this.__attachAction(triggerId, action.id);
125+
126+
return action;
127+
};
128+
129+
public readonly removeTriggerAction = <TriggerId extends keyof TriggerContextMapping>(
130+
triggerId: TriggerId,
131+
actionId: string
132+
) => {
133+
this.detachAction(triggerId, actionId);
134+
this.unregisterAction(actionId);
135+
};
136+
137+
// public readonly removeTriggerAction =
138+
139+
protected readonly __attachAction = <TriggerId extends keyof TriggerContextMapping>(
140+
triggerId: TriggerId,
141+
actionId: string
142+
): void => {
143+
const actionIds = this.triggerToActions.get(triggerId);
144+
145+
if (!actionIds!.find(id => id === actionId)) {
146+
this.triggerToActions.set(triggerId, [...actionIds!, actionId]);
147+
}
148+
};
149+
107150
public readonly getAction = <T extends AnyActionDefinition>(id: string): ActionInternal<T> => {
108151
if (!this.actions.has(id)) {
109152
throw new Error(`Action [action.id = ${id}] not registered.`);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { i18n } from '@kbn/i18n';
8+
9+
export const txtDisplayName = i18n.translate(
10+
'xpack.dashboard.panel.openFlyoutEditDrilldown.displayName',
11+
{
12+
defaultMessage: 'Manage drilldowns',
13+
}
14+
);

โ€Žx-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/index.tsxโ€Ž

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,24 @@
55
*/
66

77
import React from 'react';
8-
import { i18n } from '@kbn/i18n';
98
import { CoreStart } from 'src/core/public';
10-
import { EuiNotificationBadge } from '@elastic/eui';
119
import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public';
1210
import {
1311
reactToUiComponent,
1412
toMountPoint,
1513
} from '../../../../../../../../src/plugins/kibana_react/public';
16-
import { IEmbeddable } from '../../../../../../../../src/plugins/embeddable/public';
14+
import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
1715
import { DrilldownsStartContract } from '../../../../../../drilldowns/public';
16+
import { txtDisplayName } from './i18n';
17+
import { MenuItem } from './menu_item';
1818

1919
export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN';
2020

21-
export interface FlyoutEditDrilldownActionContext {
22-
embeddable: IEmbeddable;
23-
}
24-
25-
const drilldownsData = [{}, {}];
26-
2721
export interface FlyoutEditDrilldownParams {
2822
overlays: () => Promise<CoreStart['overlays']>;
2923
drilldowns: () => Promise<DrilldownsStartContract>;
3024
}
3125

32-
const displayName = i18n.translate('xpack.dashboard.panel.openFlyoutEditDrilldown.displayName', {
33-
defaultMessage: 'Manage drilldowns',
34-
});
35-
3626
export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOUT_EDIT_DRILLDOWN> {
3727
public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN;
3828
public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN;
@@ -41,31 +31,23 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
4131
constructor(protected readonly params: FlyoutEditDrilldownParams) {}
4232

4333
public getDisplayName() {
44-
return displayName;
34+
return txtDisplayName;
4535
}
4636

4737
public getIconType() {
4838
return 'list';
4939
}
5040

51-
private ReactComp: React.FC<{ context: FlyoutEditDrilldownActionContext }> = () => {
52-
return (
53-
<>
54-
{displayName}{' '}
55-
<EuiNotificationBadge color="subdued" style={{ float: 'right' }}>
56-
{drilldownsData.length}
57-
</EuiNotificationBadge>
58-
</>
59-
);
60-
};
41+
MenuItem = reactToUiComponent(MenuItem);
6142

62-
MenuItem = reactToUiComponent(this.ReactComp);
43+
public async isCompatible({ embeddable }: EmbeddableContext) {
44+
if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false;
45+
if (!embeddable.dynamicActions) return false;
6346

64-
public async isCompatible({ embeddable }: FlyoutEditDrilldownActionContext) {
65-
return embeddable.getInput().viewMode === 'edit' && drilldownsData.length > 0;
47+
return (await embeddable.dynamicActions.count()) > 0;
6648
}
6749

68-
public async execute(context: FlyoutEditDrilldownActionContext) {
50+
public async execute(context: EmbeddableContext) {
6951
const overlays = await this.params.overlays();
7052
const drilldowns = await this.params.drilldowns();
7153

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { EuiNotificationBadge } from '@elastic/eui';
9+
import useMountedState from 'react-use/lib/useMountedState';
10+
import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public';
11+
import { txtDisplayName } from './i18n';
12+
13+
export const MenuItem: React.FC<{ context: EmbeddableContext }> = ({ context }) => {
14+
const isMounted = useMountedState();
15+
const [count, setCount] = React.useState(0);
16+
17+
React.useEffect(() => {
18+
if (!context.embeddable.dynamicActions) return;
19+
context.embeddable.dynamicActions.count().then(result => {
20+
if (!isMounted()) return;
21+
setCount(result);
22+
});
23+
}, [context.embeddable.dynamicActions, isMounted]);
24+
25+
const badge = !count ? null : (
26+
<EuiNotificationBadge style={{ float: 'right' }}>{count}</EuiNotificationBadge>
27+
);
28+
29+
return (
30+
<>
31+
{txtDisplayName} {badge}
32+
</>
33+
);
34+
};

0 commit comments

Comments
ย (0)