Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic uiActions & license support #68507

Merged
merged 22 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
10d1323
uiActions & license support
Dosant Jun 8, 2020
03792f0
fixes
Dosant Jun 8, 2020
2f879a4
Merge branch 'master' of github.com:elastic/kibana into dev/uiactions…
Dosant Jun 11, 2020
9636632
review
Dosant Jun 11, 2020
8852123
improve sorting, improving showing error in a list
Dosant Jun 11, 2020
794a9d7
a bit of space of error icon
Dosant Jun 11, 2020
be2449d
Merge branch 'master' into dev/uiactions-license
elasticmachine Jun 15, 2020
0787389
Merge branch 'master' of github.com:elastic/kibana into dev/uiactions…
Dosant Jun 17, 2020
a9efdc0
Merge branch 'dev/uiactions-license' of github.com:Dosant/kibana into…
Dosant Jun 17, 2020
41ff5c3
refactor using `registerActionHook` approach
Dosant Jun 17, 2020
5eea2b8
fix ts
Dosant Jun 17, 2020
f77c4cf
Merge branch 'master' of github.com:elastic/kibana into dev/uiactions…
Dosant Jun 17, 2020
fa41858
@streamich review
Dosant Jun 17, 2020
a58692c
Merge branch 'master' of github.com:elastic/kibana into dev/uiactions…
Dosant Jun 23, 2020
e655e68
Merge branch 'master' of github.com:elastic/kibana into dev/uiactions…
Dosant Jun 24, 2020
fa1dcf2
fix i18n
Dosant Jun 24, 2020
2278f55
fix i18n
Dosant Jun 24, 2020
9c4a462
Merge branch 'master' into dev/uiactions-license
elasticmachine Jun 25, 2020
a9ee244
Merge branch 'master' of github.com:elastic/kibana into dev/uiactions…
Dosant Jun 26, 2020
8144666
Merge branch 'dev/uiactions-license' of github.com:Dosant/kibana into…
Dosant Jun 26, 2020
491fee6
simplify uiActions license implementation after Stacey review
Dosant Jun 26, 2020
7f02c0f
clean up
Dosant Jun 26, 2020
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
7 changes: 7 additions & 0 deletions src/plugins/ui_actions/public/actions/action_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import { Presentable } from '../util/presentable';
import { uiToReactComponent } from '../../../kibana_react/public';
import { ActionType } from '../types';

/**
* @remarks
* This is exported from a plugin only to be used in `x-pack/actions_enhanced`
* This is not part of public api and could be changed without notice
*
* @internal
*/
export class ActionInternal<A extends ActionDefinition = ActionDefinition>
implements Action<Context<A>>, Presentable<Context<A>> {
constructor(public readonly definition: A) {}
Expand Down
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 @@ -45,4 +45,4 @@ export {
applyFilterTrigger,
} from './triggers';
export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types';
export { ActionByType } from './actions';
export { ActionByType, ActionInternal } from './actions';
1 change: 1 addition & 0 deletions src/plugins/ui_actions/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const createSetupContract = (): Setup => {
registerAction: jest.fn(),
registerTrigger: jest.fn(),
unregisterAction: jest.fn(),
setCustomActionCreator: jest.fn(),
};
return setupContract;
};
Expand Down
7 changes: 6 additions & 1 deletion src/plugins/ui_actions/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ export type UiActionsSetup = Pick<
| 'registerAction'
| 'registerTrigger'
| 'unregisterAction'
| 'setCustomActionCreator'
>;

export type UiActionsStart = PublicMethodsOf<UiActionsService>;
export type UiActionsStart = Omit<
PublicMethodsOf<UiActionsService>,
'ensureActionsExist' | 'setCustomActionCreator'
>;

export class UiActionsPlugin implements Plugin<UiActionsSetup, UiActionsStart> {
private readonly service = new UiActionsService();
Expand All @@ -46,6 +50,7 @@ export class UiActionsPlugin implements Plugin<UiActionsSetup, UiActionsStart> {
}

public start(core: CoreStart): UiActionsStart {
this.service.ensureActionsExist();
return this.service;
}

Expand Down
38 changes: 31 additions & 7 deletions src/plugins/ui_actions/public/service/ui_actions_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { UiActionsService } from './ui_actions_service';
import { Action, ActionInternal, createAction } from '../actions';
import { createHelloWorldAction } from '../tests/test_samples';
import { ActionRegistry, TriggerRegistry, TriggerId, ActionType } from '../types';
import { TriggerRegistry, TriggerId, ActionType, ActionDefinitionRegistry } from '../types';
import { Trigger } from '../triggers';

// Casting to ActionType or TriggerId is a hack - in a real situation use
Expand Down Expand Up @@ -103,9 +103,9 @@ describe('UiActionsService', () => {
});
});

test('return action instance', () => {
test('returns an action instance getter', () => {
const service = new UiActionsService();
const action = service.registerAction({
const getAction = service.registerAction({
id: 'test',
execute: async () => {},
getDisplayName: () => 'test',
Expand All @@ -114,6 +114,8 @@ describe('UiActionsService', () => {
type: 'test' as ActionType,
});

const action = getAction();

expect(action).toBeInstanceOf(ActionInternal);
expect(action.id).toBe('test');
});
Expand Down Expand Up @@ -172,8 +174,8 @@ describe('UiActionsService', () => {

describe('.getTriggerCompatibleActions()', () => {
test('can register and get actions', async () => {
const actions: ActionRegistry = new Map();
const service = new UiActionsService({ actions });
const actions: ActionDefinitionRegistry = new Map();
const service = new UiActionsService({ actionDefinitions: actions });
const helloWorldAction = createHelloWorldAction({} as any);
const length = actions.size;

Expand Down Expand Up @@ -382,8 +384,8 @@ describe('UiActionsService', () => {
});

test('can register action', () => {
const actions: ActionRegistry = new Map();
const service = new UiActionsService({ actions });
const actions: ActionDefinitionRegistry = new Map();
const service = new UiActionsService({ actionDefinitions: actions });

service.registerAction({
id: ACTION_HELLO_WORLD,
Expand Down Expand Up @@ -491,4 +493,26 @@ describe('UiActionsService', () => {
);
});
});

describe('custom action creator', () => {
test('can provide custom action creator', () => {
const service = new UiActionsService();
class CustomActionInternal extends ActionInternal {
readonly custom = true;
}
service.setCustomActionCreator((def) => new CustomActionInternal(def));
const actionDef: Action = {
id: 'action1',
order: 1,
type: 'type1' as ActionType,
execute: async () => {},
getDisplayName: () => 'test',
getIconType: () => '',
isCompatible: async () => true,
};
service.registerAction(actionDef);
const action = service.getAction(actionDef.id);
expect('custom' in action).toBe(true);
});
});
});
67 changes: 55 additions & 12 deletions src/plugins/ui_actions/public/service/ui_actions_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
TriggerToActionsRegistry,
TriggerId,
TriggerContextMapping,
ActionDefinitionRegistry,
} from '../types';
import { ActionInternal, Action, ActionDefinition, ActionContext } from '../actions';
import { Trigger, TriggerContext } from '../triggers/trigger';
Expand All @@ -32,6 +33,7 @@ import { TriggerContract } from '../triggers/trigger_contract';
export interface UiActionsServiceParams {
readonly triggers?: TriggerRegistry;
readonly actions?: ActionRegistry;
readonly actionDefinitions?: ActionDefinitionRegistry;

/**
* A 1-to-N mapping from `Trigger` to zero or more `Action`.
Expand All @@ -43,15 +45,18 @@ export class UiActionsService {
protected readonly triggers: TriggerRegistry;
protected readonly actions: ActionRegistry;
protected readonly triggerToActions: TriggerToActionsRegistry;
protected readonly actionDefinitions: ActionDefinitionRegistry;

constructor({
triggers = new Map(),
actions = new Map(),
triggerToActions = new Map(),
actionDefinitions = new Map(),
}: UiActionsServiceParams = {}) {
this.triggers = triggers;
this.actions = actions;
this.triggerToActions = triggerToActions;
this.actionDefinitions = actionDefinitions;
}

public readonly registerTrigger = (trigger: Trigger) => {
Expand All @@ -77,24 +82,25 @@ export class UiActionsService {

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

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

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

return action;
return () => {
return this.getAction(definition.id);
};
};

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

this.actions.delete(actionId);
this.actionDefinitions.delete(actionId);
};

public readonly attachAction = <T extends TriggerId>(triggerId: T, actionId: string): void => {
Expand Down Expand Up @@ -142,13 +148,14 @@ export class UiActionsService {
// by this type of trigger, typescript will complain. yay!
action: Action<TriggerContextMapping[T]>
): void => {
if (!this.actions.has(action.id)) this.registerAction(action);
if (!this.actionDefinitions.has(action.id)) this.registerAction(action);
this.attachAction(triggerId, action.id);
};

public readonly getAction = <T extends ActionDefinition>(
id: string
): Action<ActionContext<T>> => {
this.ensureActionExists(id);
if (!this.actions.has(id)) {
throw new Error(`Action [action.id = ${id}] not registered.`);
}
Expand All @@ -164,9 +171,7 @@ export class UiActionsService {

const actionIds = this.triggerToActions.get(triggerId);

const actions = actionIds!
.map((actionId) => this.actions.get(actionId) as ActionInternal)
.filter(Boolean);
const actions = actionIds!.map((actionId) => this.getAction(actionId));

return actions as Array<Action<TriggerContext<T>>>;
};
Expand Down Expand Up @@ -204,6 +209,7 @@ export class UiActionsService {
this.actions.clear();
this.triggers.clear();
this.triggerToActions.clear();
this.actionDefinitions.clear();
};

/**
Expand All @@ -215,12 +221,49 @@ export class UiActionsService {
const triggers: TriggerRegistry = new Map();
const actions: ActionRegistry = new Map();
const triggerToActions: TriggerToActionsRegistry = new Map();
const actionDefinitions: ActionDefinitionRegistry = new Map();

for (const [key, value] of this.triggers.entries()) triggers.set(key, value);
for (const [key, value] of this.actions.entries()) actions.set(key, value);
for (const [key, value] of this.triggerToActions.entries())
triggerToActions.set(key, [...value]);
for (const [key, value] of this.actionDefinitions.entries()) actionDefinitions.set(key, value);
return new UiActionsService({ triggers, actions, triggerToActions, actionDefinitions });
};

/**
* Create action from action definition
* @param def
*/
private createAction = (def: ActionDefinition) =>
this.customActionCreator ? this.customActionCreator(def) : new ActionInternal(def);

/**
* Allows to override default action creator
*/
private customActionCreator?: (def: ActionDefinition) => ActionInternal;

return new UiActionsService({ triggers, actions, triggerToActions });
/**
* Allows to overrides default action creator
* @param actionCreator
*/
public setCustomActionCreator = (actionCreator: (def: ActionDefinition) => ActionInternal) => {
if (this.customActionCreator) {
throw new Error('Custom action creator is already set, and can only be set once');
}
this.customActionCreator = actionCreator;
};

// These two functions are only to support legacy plugins registering factories after the start lifecycle.
public ensureActionsExist() {
this.actionDefinitions.forEach((def) => this.ensureActionExists(def.id));
}

private ensureActionExists(actionId: string) {
if (!this.actions.has(actionId)) {
const def = this.actionDefinitions.get(actionId);
if (!def) return;
this.actions.set(actionId, this.createAction(def));
}
}
}
2 changes: 2 additions & 0 deletions src/plugins/ui_actions/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import { Filter } from '../../data/public';
import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers';
import { IEmbeddable } from '../../embeddable/public';
import { RangeSelectTriggerContext, ValueClickTriggerContext } from '../../embeddable/public';
import { ActionDefinition } from './actions';

export type TriggerRegistry = Map<TriggerId, TriggerInternal<any>>;
export type ActionRegistry = Map<string, ActionInternal>;
export type ActionDefinitionRegistry = Map<string, ActionDefinition>;
export type TriggerToActionsRegistry = Map<TriggerId, string[]>;

const DEFAULT_TRIGGER = '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export class DashboardToUrlDrilldown implements Drilldown<Config, ActionContext>

public readonly order = 8;

readonly minimalLicense = 'gold'; // example of minimal license support
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only thing required for consumer 🙌


public readonly getDisplayName = () => 'Go to URL (example)';

public readonly euiIcon = 'link';
Expand Down
14 changes: 13 additions & 1 deletion x-pack/plugins/licensing/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BehaviorSubject } from 'rxjs';
import { LicensingPluginSetup } from './types';
import { LicensingPluginSetup, LicensingPluginStart } from './types';
import { licenseMock } from '../common/licensing.mock';

const createSetupMock = () => {
Expand All @@ -18,7 +18,19 @@ const createSetupMock = () => {
return mock;
};

const createStartMock = (): LicensingPluginStart => {
Dosant marked this conversation as resolved.
Show resolved Hide resolved
const license = licenseMock.createLicense();
const mock: jest.Mocked<LicensingPluginStart> = {
license$: new BehaviorSubject(license),
refresh: jest.fn(),
};
mock.refresh.mockResolvedValue(license);

return mock;
};

export const licensingMock = {
createSetup: createSetupMock,
createStart: createStartMock,
...licenseMock,
};
3 changes: 2 additions & 1 deletion x-pack/plugins/ui_actions_enhanced/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"configPath": ["xpack", "ui_actions_enhanced"],
"requiredPlugins": [
"embeddable",
"uiActions"
"uiActions",
"licensing"
],
"server": false,
"ui": true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 { ActionEnhancedDefinition } from './action_enhanced';
import { ActionEnhancedInternal } from './action_enhanced_internal';
import { licenseMock } from '../../../licensing/common/licensing.mock';

describe('ActionEnhanced', () => {
const def: ActionEnhancedDefinition = {
id: 'test',
async execute() {},
};

describe('License checks inside isCompatible', () => {
test('no license requirements', async () => {
const action = new ActionEnhancedInternal(def, () => licenseMock.createLicense());
expect(await action.isCompatible({})).toBe(true);
});

test('not enough license level', async () => {
const action = new ActionEnhancedInternal({ ...def, minimalLicense: 'gold' }, () =>
licenseMock.createLicense()
);
expect(await action.isCompatible({})).toBe(false);
});

test('enough license level', async () => {
const action = new ActionEnhancedInternal({ ...def, minimalLicense: 'gold' }, () =>
licenseMock.createLicense({ license: { type: 'gold' } })
);
expect(await action.isCompatible({})).toBe(true);
});
});
});
Loading