Skip to content

Commit

Permalink
Drilldown events 4 (#59876)
Browse files Browse the repository at this point in the history
* feat: 🎸 mock sample drilldown execute methods

* feat: 🎸 add .dynamicActions manager to Embeddable

* feat: 🎸 add first version of dynamic action manager
  • Loading branch information
streamich authored Mar 11, 2020
1 parent 2e65fd0 commit beb053b
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
timeFieldName: this.vis.indexPattern.timeFieldName,
data: event.data,
};

getUiActions()
.getTrigger(triggerId)
.exec(context);
Expand Down
25 changes: 21 additions & 4 deletions src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import { IContainer } from '../containers';
import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable';
import { ViewMode } from '../types';
import { EmbeddableActionStorage } from './embeddable_action_storage';
import { UiActionsStart } from '../ui_actions';
import {
UiActionsStart,
UiActionsDynamicActionManager,
} from '../../../../../plugins/ui_actions/public';

function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title;
Expand Down Expand Up @@ -55,9 +58,18 @@ export abstract class Embeddable<
// TODO: Rename to destroyed.
private destoyed: boolean = false;

private __actionStorage?: EmbeddableActionStorage;
public get actionStorage(): EmbeddableActionStorage {
return this.__actionStorage || (this.__actionStorage = new EmbeddableActionStorage(this));
private __dynamicActions?: UiActionsDynamicActionManager;
public get dynamicActions(): UiActionsDynamicActionManager | undefined {
if (!this.params.uiActions) return undefined;
if (!this.__dynamicActions) {
this.__dynamicActions = new UiActionsDynamicActionManager({
isCompatible: async () => true,
storage: new EmbeddableActionStorage(this),
uiActions: this.params.uiActions,
});
}

return this.__dynamicActions;
}

constructor(
Expand All @@ -66,6 +78,7 @@ 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 @@ -89,6 +102,10 @@ export abstract class Embeddable<
this.onResetInput(newInput);
});
}

if (this.dynamicActions) {
this.dynamicActions.start();
}
}

public getIsContainer(): this is IContainer {
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import { Observable } from 'rxjs';
import { UiActionsDynamicActionManager } from '../../../../../plugins/ui_actions/public';
import { Adapters } from '../types';
import { IContainer } from '../containers/i_container';
import { ViewMode } from '../types';
Expand Down Expand Up @@ -82,6 +83,11 @@ export interface IEmbeddable<
**/
readonly id: string;

/**
* Default implementation of dynamic action API for embeddables.
*/
dynamicActions?: UiActionsDynamicActionManager;

/**
* A functional representation of the isContainer variable, but helpful for typescript to
* know the shape if this returns true
Expand Down
9 changes: 6 additions & 3 deletions src/plugins/ui_actions/public/actions/action_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@

import { uiToReactComponent } from '../../../kibana_react/public';
import { Presentable } from '../util/presentable';
import { ActionDefinition } from './action_definition';
import { ActionDefinition } from './action';
import {
AnyActionFactoryDefinition,
AFDConfig as Config,
AFDFactoryContext as FactoryContext,
AFDActionContext as ActionContext,
} from './action_factory_definition';
import { Configurable } from '../util';
import { SerializedAction } from './types';

export class ActionFactory<D extends AnyActionFactoryDefinition>
implements Presentable<FactoryContext<D>>, Configurable<Config<D>> {
Expand Down Expand Up @@ -62,8 +63,10 @@ export class ActionFactory<D extends AnyActionFactoryDefinition>
return this.definition.getHref(context);
}

public create(config: Config<D>): ActionDefinition<ActionContext<D>> {
return this.definition.create(config);
public create(
serializedAction: Omit<SerializedAction<Config<D>>, 'factoryId'>
): ActionDefinition<ActionContext<D>> {
return this.definition.create(serializedAction);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
* under the License.
*/

import { ActionDefinition } from './action_definition';
import { ActionDefinition } from './action';
import { Presentable, Configurable } from '../util';
import { SerializedAction } from './types';

/**
* This is a convenience interface for registering new action factories.
Expand All @@ -39,7 +40,9 @@ export interface ActionFactoryDefinition<
* This method should return a definition of a new action, normally used to
* register it in `ui_actions` registry.
*/
create(config: Config): ActionDefinition<any>; // TODO: FIX THIS....
create(
serializedAction: Omit<SerializedAction<Config>, 'factoryId'>
): ActionDefinition<ActionContext>;
}

export type AnyActionFactoryDefinition = ActionFactoryDefinition<any, any, any>;
Expand Down
17 changes: 5 additions & 12 deletions src/plugins/ui_actions/public/actions/action_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Action, ActionContext as Context, AnyActionDefinition } from './action'
import { Presentable } from '../util/presentable';
import { uiToReactComponent } from '../../../kibana_react/public';
import { ActionType } from '../types';
import { SerializedAction } from './types';

export class ActionInternal<A extends AnyActionDefinition>
implements Action<Context<A>>, Presentable<Context<A>> {
Expand All @@ -45,7 +46,7 @@ export class ActionInternal<A extends AnyActionDefinition>
}

public getDisplayName(context: Context<A>): string {
if (!this.definition.getDisplayName) return '';
if (!this.definition.getDisplayName) return `Action: ${this.id}`;
return this.definition.getDisplayName(context);
}

Expand All @@ -64,8 +65,7 @@ export class ActionInternal<A extends AnyActionDefinition>
throw new Error('Action does not have a config.');
}

const serialized: SerializedAction = {
id: this.id,
const serialized: SerializedAction<unknown> = {
factoryId: this.type,
name: this.name,
config: this.config,
Expand All @@ -74,17 +74,10 @@ export class ActionInternal<A extends AnyActionDefinition>
return serialized;
}

public deserialize({ name, config }: SerializedAction) {
public deserialize({ name, config }: SerializedAction<unknown>) {
this.name = name;
this.config = config;
this.config = config as object;
}
}

export type AnyActionInternal = ActionInternal<any>;

export interface SerializedAction<Config extends object = object> {
readonly id: string;
readonly factoryId: string;
readonly name: string;
readonly config: Config;
}
13 changes: 8 additions & 5 deletions src/plugins/ui_actions/public/actions/create_action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
* under the License.
*/

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

export function createAction<T extends ActionType>(action: ActionDefinition<T>): ActionByType<T> {
export function createAction<T extends keyof TriggerContextMapping>(
action: ActionDefinition<Ensure<TriggerContextMapping[T], object>>
): Action<TriggerContextMapping[T], T> {
return {
getIconType: () => undefined,
order: 0,
Expand All @@ -30,5 +33,5 @@ export function createAction<T extends ActionType>(action: ActionDefinition<T>):
getDisplayName: () => '',
getHref: () => undefined,
...action,
} as ActionByType<T>;
} as Action<TriggerContextMapping[T], T>;
}
67 changes: 65 additions & 2 deletions src/plugins/ui_actions/public/actions/dynamic_action_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,77 @@
* under the License.
*/

import { ActionStorage } from './dynamic_action_storage';
import { v4 as uuidv4 } from 'uuid';
import { ActionStorage, SerializedEvent } from './dynamic_action_storage';
import { UiActionsService } from '../service';
import { SerializedAction } from './types';
import { ActionDefinition } from './action';

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

export class DynamicActionManager {
static idPrefixCounter = 0;

private readonly idPrefix = 'DYN_ACTION_' + DynamicActionManager.idPrefixCounter++;

constructor(protected readonly params: DynamicActionManagerParams) {}

protected generateActionId(eventId: string): string {
return this.idPrefix + eventId;
}

public async start() {
const events = await this.params.storage.list();

for (const event of events) {
this.reviveAction(event);
}
}

public async stop() {
/*
const { storage, uiActions } = this.params;
const events = await storage.list();
for (const event of events) {
uiActions.detachAction(event.triggerId, event.action.id);
uiActions.unregisterAction(event.action.id);
}
*/
}

public async createEvent(action: SerializedAction<unknown>, triggerId = 'VALUE_CLICK_TRIGGER') {
const event: SerializedEvent = {
eventId: uuidv4(),
triggerId,
action,
};

await this.params.storage.create(event);
this.reviveAction(event);
}

protected reviveAction(event: SerializedEvent) {
const { eventId, triggerId, action } = event;
const { uiActions, isCompatible } = this.params;
const { name } = action;

const actionId = this.generateActionId(eventId);
const factory = uiActions.getActionFactory(event.action.factoryId);
const actionDefinition: ActionDefinition<any> = {
...factory.create(action as SerializedAction<object>),
id: actionId,
isCompatible,
getDisplayName: () => name,
getIconType: context => factory.getIconType(context),
};

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

protected killAction(actionId: string) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
* under the License.
*/

import { SerializedAction } from './action_internal';
import { SerializedAction } from './types';

/**
* Serialized representation of event-action pair, used to persist in storage.
*/
export interface SerializedEvent {
eventId: string;
triggerId: string;
action: SerializedAction;
action: SerializedAction<unknown>;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/ui_actions/public/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ export * from './action_factory';
export * from './create_action';
export * from './incompatible_action_error';
export * from './dynamic_action_storage';
export * from './dynamic_action_manager';
export * from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,8 @@
* under the License.
*/

import { ActionType, ActionContextMapping } from '../types';
import { Presentable } from '../util/presentable';

export interface ActionDefinition<T extends ActionType>
extends Partial<Presentable<ActionContextMapping[T]>> {
/**
* ID of the action factory for this action. Action factories are registered
* int X-Pack `ui_actions` plugin.
*/
readonly type?: T;

/**
* Executes the action.
*/
execute(context: ActionContextMapping[T]): Promise<void>;
export interface SerializedAction<Config> {
readonly factoryId: string;
readonly name: string;
readonly config: Config;
}
2 changes: 1 addition & 1 deletion src/plugins/ui_actions/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ export {
} from './util';
export { Trigger, TriggerContext } from './triggers';
export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types';
export { ActionByType } from './actions';
export { ActionByType, DynamicActionManager as UiActionsDynamicActionManager } from './actions';
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ describe('UiActionsService', () => {
type: 'test' as ActionType,
});
});

test.todo('return action instance');
});

describe('.getTriggerActions()', () => {
Expand Down Expand Up @@ -481,5 +483,7 @@ describe('UiActionsService', () => {
test.todo('.getActionFactories() returns empty array if no action factories registered');
test.todo('can register an action factory');
test.todo('can retrieve all action factories');
test.todo('can retrieve action factory by ID');
test.todo('throws when retrieving action factory that does not exist');
});
});
20 changes: 18 additions & 2 deletions src/plugins/ui_actions/public/service/ui_actions_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,18 @@ export class UiActionsService {
return trigger.contract;
};

public readonly registerAction = <A extends AnyActionDefinition>(definition: A) => {
public readonly registerAction = <A extends AnyActionDefinition>(
definition: A
): ActionInternal<A> => {
if (this.actions.has(definition.id)) {
throw new Error(`Action [action.id = ${definition.id}] already registered.`);
}

this.actions.set(definition.id, new ActionInternal(definition));
const action = new ActionInternal(definition);

this.actions.set(action.id, action);

return action;
};

public readonly getAction = <T extends AnyActionDefinition>(id: string): ActionInternal<T> => {
Expand Down Expand Up @@ -237,6 +243,16 @@ export class UiActionsService {
this.actionFactories.set(actionFactory.id, actionFactory);
};

public readonly getActionFactory = (actionFactoryId: string): AnyActionFactory => {
const actionFactory = this.actionFactories.get(actionFactoryId);

if (!actionFactory) {
throw new Error(`Action factory [actionFactoryId = ${actionFactoryId}] does not exist.`);
}

return actionFactory;
};

/**
* Returns an array of all action factories.
*/
Expand Down
Loading

0 comments on commit beb053b

Please sign in to comment.