Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export abstract class Embeddable<
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput
> implements IEmbeddable<TEmbeddableInput, TEmbeddableOutput> {
static runtimeId: number = 0;

public readonly runtimeId = Embeddable.runtimeId++;

public readonly parent?: IContainer;
public readonly isContainer: boolean = false;
public abstract readonly type: string;
Expand All @@ -63,7 +67,7 @@ export abstract class Embeddable<
if (!this.params.uiActions) return undefined;
if (!this.__dynamicActions) {
this.__dynamicActions = new UiActionsDynamicActionManager({
isCompatible: async () => true,
isCompatible: async ({ embeddable }: any) => embeddable.runtimeId === this.runtimeId,
storage: new EmbeddableActionStorage(this),
uiActions: this.params.uiActions,
});
Expand All @@ -78,7 +82,6 @@ export abstract class Embeddable<
parent?: IContainer,
public readonly params: EmbeddableParams = {}
) {
window.emb = this;
this.id = input.id;
this.output = {
title: getPanelTitle(input, output),
Expand All @@ -104,7 +107,12 @@ export abstract class Embeddable<
}

if (this.dynamicActions) {
this.dynamicActions.start();
this.dynamicActions.start().catch(error => {
/* eslint-disable */
console.log('Failed to start embeddable dynamic actions', this);
console.error(error);
/* eslint-enable */
});
}
}

Expand Down Expand Up @@ -184,6 +192,16 @@ export abstract class Embeddable<
*/
public destroy(): void {
this.destoyed = true;

if (this.dynamicActions) {
this.dynamicActions.stop().catch(error => {
/* eslint-disable */
console.log('Failed to stop embeddable dynamic actions', this);
console.error(error);
/* eslint-enable */
});
}

if (this.parentSubscription) {
this.parentSubscription.unsubscribe();
}
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ export interface IEmbeddable<
**/
readonly id: string;

/**
* Unique ID an embeddable is assigned each time it is initialized. This ID
* is different for different instances of the same embeddable. For example,
* if the same dashboard is rendered twice on the screen, all embeddable
* instances will have a unique `runtimeId`.
*/
readonly runtimeId?: number;

/**
* Default implementation of dynamic action API for embeddables.
*/
Expand Down
19 changes: 12 additions & 7 deletions src/plugins/ui_actions/public/actions/create_action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@
* under the License.
*/

import { Ensure } from '@kbn/utility-types';
import { Action } from './action';
import { TriggerContextMapping } from '../types';
import { ActionContextMapping } from '../types';
import { ActionByType } from './action';
import { ActionType } from '../types';
import { ActionDefinition } from './action';

export function createAction<T extends keyof TriggerContextMapping>(
action: ActionDefinition<Ensure<TriggerContextMapping[T], object>>
): Action<TriggerContextMapping[T], T> {
interface ActionDefinitionByType<T extends ActionType>
extends Omit<ActionDefinition<ActionContextMapping[T]>, 'id'> {
id?: string;
}

export function createAction<T extends ActionType>(
action: ActionDefinitionByType<T>
): ActionByType<T> {
return {
getIconType: () => undefined,
order: 0,
Expand All @@ -33,5 +38,5 @@ export function createAction<T extends keyof TriggerContextMapping>(
getDisplayName: () => '',
getHref: () => undefined,
...action,
} as Action<TriggerContextMapping[T], T>;
} as ActionByType<T>;
}
27 changes: 17 additions & 10 deletions src/plugins/ui_actions/public/actions/dynamic_action_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ import { ActionDefinition } from './action';

export interface DynamicActionManagerParams {
storage: ActionStorage;
uiActions: Pick<UiActionsService, 'registerAction' | 'attachAction' | 'getActionFactory'>;
uiActions: Pick<
UiActionsService,
'addTriggerAction' | 'removeTriggerAction' | 'getActionFactory'
>;
isCompatible: <C = unknown>(context: C) => Promise<boolean>;
}

export class DynamicActionManager {
static idPrefixCounter = 0;

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

constructor(protected readonly params: DynamicActionManagerParams) {}

Expand All @@ -49,15 +52,11 @@ export class DynamicActionManager {
}

public async stop() {
/*
const { storage, uiActions } = this.params;
const events = await storage.list();
const events = await this.params.storage.list();

for (const event of events) {
uiActions.detachAction(event.triggerId, event.action.id);
uiActions.unregisterAction(event.action.id);
this.killAction(event);
}
*/
}

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

public async count(): Promise<number> {
return await this.params.storage.count();
}

protected reviveAction(event: SerializedEvent) {
const { eventId, triggerId, action } = event;
const { uiActions, isCompatible } = this.params;
Expand All @@ -86,8 +89,12 @@ export class DynamicActionManager {
getIconType: context => factory.getIconType(context),
};

uiActions.attachAction(triggerId as any, actionDefinition as any);
uiActions.addTriggerAction(triggerId as any, actionDefinition);
}

protected killAction(actionId: string) {}
protected killAction({ eventId, triggerId }: SerializedEvent) {
const { uiActions } = this.params;
const actionId = this.generateActionId(eventId);
uiActions.removeTriggerAction(triggerId as any, actionId);
}
}
15 changes: 9 additions & 6 deletions src/plugins/ui_actions/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,22 @@ const createSetupContract = (): Setup => {

const createStartContract = (): Start => {
const startContract: Start = {
addTriggerAction: jest.fn(),
attachAction: jest.fn(),
registerAction: jest.fn(),
registerTrigger: jest.fn(),
registerActionFactory: jest.fn(),
getAction: jest.fn(),
clear: jest.fn(),
detachAction: jest.fn(),
executeTriggerActions: jest.fn(),
fork: jest.fn(),
getAction: jest.fn(),
getActionFactories: jest.fn(),
getActionFactory: jest.fn(),
getTrigger: jest.fn(),
getTriggerActions: jest.fn((id: TriggerId) => []),
getTriggerCompatibleActions: jest.fn(),
clear: jest.fn(),
fork: jest.fn(),
registerAction: jest.fn(),
registerActionFactory: jest.fn(),
registerTrigger: jest.fn(),
removeTriggerAction: jest.fn(),
};

return startContract;
Expand Down
43 changes: 43 additions & 0 deletions src/plugins/ui_actions/public/service/ui_actions_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
AnyActionFactoryDefinition,
ActionFactory,
AnyActionFactory,
ActionDefinition,
} from '../actions';
import { Trigger, TriggerContext } from '../triggers/trigger';
import { TriggerInternal } from '../triggers/trigger_internal';
Expand Down Expand Up @@ -104,6 +105,48 @@ export class UiActionsService {
return action;
};

protected readonly unregisterAction = (actionId: string): void => {
if (!this.actions.has(actionId)) {
throw new Error(`Action [action.id = ${actionId}] is not registered.`);
}

this.actions.delete(actionId);
};

public readonly addTriggerAction = <TriggerId extends keyof TriggerContextMapping>(
triggerId: TriggerId,
definition: ActionDefinition<TriggerContextMapping[TriggerId]>
) => {
// Check if trigger exists, if not, next line throws.
this.getTrigger(triggerId);

const action = this.registerAction(definition);
this.__attachAction(triggerId, action.id);

return action;
};

public readonly removeTriggerAction = <TriggerId extends keyof TriggerContextMapping>(
triggerId: TriggerId,
actionId: string
) => {
this.detachAction(triggerId, actionId);
this.unregisterAction(actionId);
};

// public readonly removeTriggerAction =

protected readonly __attachAction = <TriggerId extends keyof TriggerContextMapping>(
triggerId: TriggerId,
actionId: string
): void => {
const actionIds = this.triggerToActions.get(triggerId);

if (!actionIds!.find(id => id === actionId)) {
this.triggerToActions.set(triggerId, [...actionIds!, actionId]);
}
};

public readonly getAction = <T extends AnyActionDefinition>(id: string): ActionInternal<T> => {
if (!this.actions.has(id)) {
throw new Error(`Action [action.id = ${id}] not registered.`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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';

export const txtDisplayName = i18n.translate(
'xpack.dashboard.panel.openFlyoutEditDrilldown.displayName',
{
defaultMessage: 'Manage drilldowns',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,24 @@
*/

import React from 'react';
import { i18n } from '@kbn/i18n';
import { CoreStart } from 'src/core/public';
import { EuiNotificationBadge } from '@elastic/eui';
import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public';
import {
reactToUiComponent,
toMountPoint,
} from '../../../../../../../../src/plugins/kibana_react/public';
import { IEmbeddable } from '../../../../../../../../src/plugins/embeddable/public';
import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
import { DrilldownsStartContract } from '../../../../../../drilldowns/public';
import { txtDisplayName } from './i18n';
import { MenuItem } from './menu_item';

export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN';

export interface FlyoutEditDrilldownActionContext {
embeddable: IEmbeddable;
}

const drilldownsData = [{}, {}];

export interface FlyoutEditDrilldownParams {
overlays: () => Promise<CoreStart['overlays']>;
drilldowns: () => Promise<DrilldownsStartContract>;
}

const displayName = i18n.translate('xpack.dashboard.panel.openFlyoutEditDrilldown.displayName', {
defaultMessage: 'Manage drilldowns',
});

export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOUT_EDIT_DRILLDOWN> {
public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN;
public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN;
Expand All @@ -41,31 +31,23 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
constructor(protected readonly params: FlyoutEditDrilldownParams) {}

public getDisplayName() {
return displayName;
return txtDisplayName;
}

public getIconType() {
return 'list';
}

private ReactComp: React.FC<{ context: FlyoutEditDrilldownActionContext }> = () => {
return (
<>
{displayName}{' '}
<EuiNotificationBadge color="subdued" style={{ float: 'right' }}>
{drilldownsData.length}
</EuiNotificationBadge>
</>
);
};
MenuItem = reactToUiComponent(MenuItem);

MenuItem = reactToUiComponent(this.ReactComp);
public async isCompatible({ embeddable }: EmbeddableContext) {
if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false;
if (!embeddable.dynamicActions) return false;

public async isCompatible({ embeddable }: FlyoutEditDrilldownActionContext) {
return embeddable.getInput().viewMode === 'edit' && drilldownsData.length > 0;
return (await embeddable.dynamicActions.count()) > 0;
}

public async execute(context: FlyoutEditDrilldownActionContext) {
public async execute(context: EmbeddableContext) {
const overlays = await this.params.overlays();
const drilldowns = await this.params.drilldowns();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 { EuiNotificationBadge } from '@elastic/eui';
import useMountedState from 'react-use/lib/useMountedState';
import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public';
import { txtDisplayName } from './i18n';

export const MenuItem: React.FC<{ context: EmbeddableContext }> = ({ context }) => {
const isMounted = useMountedState();
const [count, setCount] = React.useState(0);

React.useEffect(() => {
if (!context.embeddable.dynamicActions) return;
context.embeddable.dynamicActions.count().then(result => {
if (!isMounted()) return;
setCount(result);
});
}, [context.embeddable.dynamicActions, isMounted]);

const badge = !count ? null : (
<EuiNotificationBadge style={{ float: 'right' }}>{count}</EuiNotificationBadge>
);

return (
<>
{txtDisplayName} {badge}
</>
);
};
Loading