[uiActions] notify action usage (#76294)
Notify feature usage when dynamic actions with specified license requirements are executed Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
5345af9281
commit
25c176255a
|
@ -44,6 +44,7 @@ export class DashboardToUrlDrilldown implements Drilldown<Config, UrlTrigger> {
|
|||
public readonly order = 8;
|
||||
|
||||
readonly minimalLicense = 'gold'; // example of minimal license support
|
||||
readonly licenseFeatureName = 'Sample URL Drilldown';
|
||||
|
||||
public readonly getDisplayName = () => 'Go to URL (example)';
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ const createSetupMock = (): jest.Mocked<FeatureUsageServiceSetup> => {
|
|||
register: jest.fn(),
|
||||
};
|
||||
|
||||
mock.register.mockImplementation(() => Promise.resolve());
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
|
@ -23,6 +25,8 @@ const createStartMock = (): jest.Mocked<FeatureUsageServiceStart> => {
|
|||
notifyUsage: jest.fn(),
|
||||
};
|
||||
|
||||
mock.notifyUsage.mockImplementation(() => Promise.resolve());
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
urlDrilldownActionFactory,
|
||||
} from './test_data';
|
||||
import { ActionFactory } from '../../dynamic_actions';
|
||||
import { licenseMock } from '../../../../licensing/common/licensing.mock';
|
||||
import { licensingMock } from '../../../../licensing/public/mocks';
|
||||
|
||||
// TODO: afterEach is not available for it globally during setup
|
||||
// https://github.com/elastic/kibana/issues/59469
|
||||
|
@ -68,8 +68,12 @@ test('If not enough license, button is disabled', () => {
|
|||
{
|
||||
...urlDrilldownActionFactory,
|
||||
minimalLicense: 'gold',
|
||||
licenseFeatureName: 'Url Drilldown',
|
||||
},
|
||||
() => licenseMock.createLicense()
|
||||
{
|
||||
getLicense: () => licensingMock.createLicense(),
|
||||
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
|
||||
}
|
||||
);
|
||||
const screen = render(<Demo actionFactories={[dashboardFactory, urlWithGoldLicense]} />);
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
if (
|
||||
!currentActionFactory &&
|
||||
actionFactories.length === 1 &&
|
||||
actionFactories[0].isCompatibleLicence()
|
||||
actionFactories[0].isCompatibleLicense()
|
||||
) {
|
||||
onActionFactoryChange(actionFactories[0]);
|
||||
}
|
||||
|
@ -314,8 +314,8 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
|
|||
* make sure not compatible factories are in the end
|
||||
*/
|
||||
const ensureOrder = (factories: ActionFactory[]) => {
|
||||
const compatibleLicense = factories.filter((f) => f.isCompatibleLicence());
|
||||
const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicence());
|
||||
const compatibleLicense = factories.filter((f) => f.isCompatibleLicense());
|
||||
const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicense());
|
||||
return [
|
||||
...compatibleLicense.sort((f1, f2) => f2.order - f1.order),
|
||||
...notCompatibleLicense.sort((f1, f2) => f2.order - f1.order),
|
||||
|
@ -328,7 +328,7 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
|
|||
<EuiFlexItem grow={false} key={actionFactory.id}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
!actionFactory.isCompatibleLicence() && (
|
||||
!actionFactory.isCompatibleLicense() && (
|
||||
<FormattedMessage
|
||||
defaultMessage="Insufficient license level"
|
||||
id="xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip"
|
||||
|
@ -341,7 +341,7 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
|
|||
label={actionFactory.getDisplayName(context)}
|
||||
data-test-subj={`${TEST_SUBJ_ACTION_FACTORY_ITEM}-${actionFactory.id}`}
|
||||
onClick={() => onActionFactorySelected(actionFactory)}
|
||||
disabled={!actionFactory.isCompatibleLicence()}
|
||||
disabled={!actionFactory.isCompatibleLicense()}
|
||||
>
|
||||
{actionFactory.getIconType(context) && (
|
||||
<EuiIcon type={actionFactory.getIconType(context)!} size="m" />
|
||||
|
|
|
@ -10,7 +10,7 @@ import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/p
|
|||
import { ActionWizard } from './action_wizard';
|
||||
import { ActionFactory, ActionFactoryDefinition } from '../../dynamic_actions';
|
||||
import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
import { licenseMock } from '../../../../licensing/common/licensing.mock';
|
||||
import { licensingMock } from '../../../../licensing/public/mocks';
|
||||
import {
|
||||
APPLY_FILTER_TRIGGER,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
|
@ -116,9 +116,10 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition<
|
|||
},
|
||||
};
|
||||
|
||||
export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () =>
|
||||
licenseMock.createLicense()
|
||||
);
|
||||
export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, {
|
||||
getLicense: () => licensingMock.createLicense(),
|
||||
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
|
||||
});
|
||||
|
||||
interface UrlDrilldownConfig {
|
||||
url: string;
|
||||
|
@ -176,9 +177,10 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition<UrlDrilldownConf
|
|||
},
|
||||
};
|
||||
|
||||
export const urlFactory = new ActionFactory(urlDrilldownActionFactory, () =>
|
||||
licenseMock.createLicense()
|
||||
);
|
||||
export const urlFactory = new ActionFactory(urlDrilldownActionFactory, {
|
||||
getLicense: () => licensingMock.createLicense(),
|
||||
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
|
||||
});
|
||||
|
||||
export const mockSupportedTriggers: TriggerId[] = [
|
||||
VALUE_CLICK_TRIGGER,
|
||||
|
|
|
@ -148,7 +148,7 @@ export function createFlyoutManageDrilldowns({
|
|||
icon: actionFactory?.getIconType(drilldownFactoryContext),
|
||||
error: !actionFactory
|
||||
? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development
|
||||
: !actionFactory.isCompatibleLicence()
|
||||
: !actionFactory.isCompatibleLicense()
|
||||
? insufficientLicenseLevel
|
||||
: undefined,
|
||||
triggers: drilldown.triggers.map((trigger) => getTrigger(trigger as TriggerId)),
|
||||
|
|
|
@ -75,7 +75,7 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
|
|||
);
|
||||
|
||||
const hasNotCompatibleLicenseFactory = () =>
|
||||
actionFactories?.some((f) => !f.isCompatibleLicence());
|
||||
actionFactories?.some((f) => !f.isCompatibleLicense());
|
||||
|
||||
const renderGetMoreActionsLink = () => (
|
||||
<EuiText size="s">
|
||||
|
|
|
@ -37,11 +37,18 @@ export interface DrilldownDefinition<
|
|||
id: string;
|
||||
|
||||
/**
|
||||
* Minimal licence level
|
||||
* Minimal license level
|
||||
* Empty means no restrictions
|
||||
*/
|
||||
minimalLicense?: LicenseType;
|
||||
|
||||
/**
|
||||
* Required when `minimalLicense` is used.
|
||||
* Is a user-facing string. Has to be unique. Doesn't need i18n.
|
||||
* The feature's name will be displayed to Cloud end-users when they're billed based on their feature usage.
|
||||
*/
|
||||
licenseFeatureName?: string;
|
||||
|
||||
/**
|
||||
* Determines the display order of the drilldowns in the flyout picker.
|
||||
* Higher numbers are displayed first.
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { ActionFactory } from './action_factory';
|
||||
import { ActionFactoryDefinition } from './action_factory_definition';
|
||||
import { licensingMock } from '../../../licensing/public/mocks';
|
||||
import { PublicLicense } from '../../../licensing/public';
|
||||
|
||||
const def: ActionFactoryDefinition = {
|
||||
id: 'ACTION_FACTORY_1',
|
||||
|
@ -22,34 +23,94 @@ const def: ActionFactoryDefinition = {
|
|||
supportedTriggers: () => [],
|
||||
};
|
||||
|
||||
const featureUsage = licensingMock.createStart().featureUsage;
|
||||
|
||||
const createActionFactory = (
|
||||
defOverride: Partial<ActionFactoryDefinition> = {},
|
||||
license?: Partial<PublicLicense>
|
||||
) => {
|
||||
return new ActionFactory(
|
||||
{ ...def, ...defOverride },
|
||||
{
|
||||
getLicense: () => licensingMock.createLicense({ license }),
|
||||
getFeatureUsageStart: () => featureUsage,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
describe('License & ActionFactory', () => {
|
||||
test('no license requirements', async () => {
|
||||
const factory = new ActionFactory(def, () => licensingMock.createLicense());
|
||||
const factory = createActionFactory();
|
||||
expect(await factory.isCompatible({ triggers: [] })).toBe(true);
|
||||
expect(factory.isCompatibleLicence()).toBe(true);
|
||||
expect(factory.isCompatibleLicense()).toBe(true);
|
||||
});
|
||||
|
||||
test('not enough license level', async () => {
|
||||
const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () =>
|
||||
licensingMock.createLicense()
|
||||
);
|
||||
const factory = createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'Feature' });
|
||||
expect(await factory.isCompatible({ triggers: [] })).toBe(true);
|
||||
expect(factory.isCompatibleLicence()).toBe(false);
|
||||
expect(factory.isCompatibleLicense()).toBe(false);
|
||||
});
|
||||
|
||||
test('licence has expired', async () => {
|
||||
const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () =>
|
||||
licensingMock.createLicense({ license: { type: 'gold', status: 'expired' } })
|
||||
test('license has expired', async () => {
|
||||
const factory = createActionFactory(
|
||||
{ minimalLicense: 'gold', licenseFeatureName: 'Feature' },
|
||||
{ type: 'gold', status: 'expired' }
|
||||
);
|
||||
expect(await factory.isCompatible({ triggers: [] })).toBe(true);
|
||||
expect(factory.isCompatibleLicence()).toBe(false);
|
||||
expect(factory.isCompatibleLicense()).toBe(false);
|
||||
});
|
||||
|
||||
test('enough license level', async () => {
|
||||
const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () =>
|
||||
licensingMock.createLicense({ license: { type: 'gold' } })
|
||||
const factory = createActionFactory(
|
||||
{ minimalLicense: 'gold', licenseFeatureName: 'Feature' },
|
||||
{ type: 'gold' }
|
||||
);
|
||||
|
||||
expect(await factory.isCompatible({ triggers: [] })).toBe(true);
|
||||
expect(factory.isCompatibleLicence()).toBe(true);
|
||||
expect(factory.isCompatibleLicense()).toBe(true);
|
||||
});
|
||||
|
||||
describe('licenseFeatureName', () => {
|
||||
test('licenseFeatureName is required, if minimalLicense is provided', () => {
|
||||
expect(() => {
|
||||
createActionFactory();
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'feature' });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
createActionFactory({ minimalLicense: 'gold' });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test('"licenseFeatureName"', () => {
|
||||
expect(
|
||||
createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'feature' })
|
||||
.licenseFeatureName
|
||||
).toBe('feature');
|
||||
expect(createActionFactory().licenseFeatureName).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifyFeatureUsage', () => {
|
||||
const spy = jest.spyOn(featureUsage, 'notifyUsage');
|
||||
beforeEach(() => {
|
||||
spy.mockClear();
|
||||
});
|
||||
test('is not called if no license requirements', async () => {
|
||||
const action = createActionFactory().create({ name: 'fake', config: {} });
|
||||
await action.execute({});
|
||||
expect(spy).not.toBeCalled();
|
||||
});
|
||||
test('is called if has license requirements', async () => {
|
||||
const action = createActionFactory({
|
||||
minimalLicense: 'gold',
|
||||
licenseFeatureName: 'feature',
|
||||
}).create({ name: 'fake', config: {} });
|
||||
await action.execute({});
|
||||
expect(spy).toBeCalledWith('feature');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,9 +13,14 @@ import {
|
|||
import { ActionFactoryDefinition } from './action_factory_definition';
|
||||
import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { BaseActionFactoryContext, SerializedAction } from './types';
|
||||
import { ILicense } from '../../../licensing/public';
|
||||
import { ILicense, LicensingPluginStart } from '../../../licensing/public';
|
||||
import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export interface ActionFactoryDeps {
|
||||
readonly getLicense: () => ILicense;
|
||||
readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage'];
|
||||
}
|
||||
|
||||
export class ActionFactory<
|
||||
Config extends object = object,
|
||||
SupportedTriggers extends TriggerId = TriggerId,
|
||||
|
@ -31,11 +36,18 @@ export class ActionFactory<
|
|||
FactoryContext,
|
||||
ActionContext
|
||||
>,
|
||||
protected readonly getLicence: () => ILicense
|
||||
) {}
|
||||
protected readonly deps: ActionFactoryDeps
|
||||
) {
|
||||
if (def.minimalLicense && !def.licenseFeatureName) {
|
||||
throw new Error(
|
||||
`ActionFactory [actionFactory.id = ${def.id}] "licenseFeatureName" is required, if "minimalLicense" is provided`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly id = this.def.id;
|
||||
public readonly minimalLicense = this.def.minimalLicense;
|
||||
public readonly licenseFeatureName = this.def.licenseFeatureName;
|
||||
public readonly order = this.def.order || 0;
|
||||
public readonly MenuItem? = this.def.MenuItem;
|
||||
public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined;
|
||||
|
@ -65,13 +77,13 @@ export class ActionFactory<
|
|||
}
|
||||
|
||||
/**
|
||||
* Does this action factory licence requirements
|
||||
* Does this action factory license requirements
|
||||
* compatible with current license?
|
||||
*/
|
||||
public isCompatibleLicence() {
|
||||
public isCompatibleLicense() {
|
||||
if (!this.minimalLicense) return true;
|
||||
const licence = this.getLicence();
|
||||
return licence.isAvailable && licence.isActive && licence.hasAtLeast(this.minimalLicense);
|
||||
const license = this.deps.getLicense();
|
||||
return license.isAvailable && license.isActive && license.hasAtLeast(this.minimalLicense);
|
||||
}
|
||||
|
||||
public create(
|
||||
|
@ -81,14 +93,31 @@ export class ActionFactory<
|
|||
return {
|
||||
...action,
|
||||
isCompatible: async (context: ActionContext): Promise<boolean> => {
|
||||
if (!this.isCompatibleLicence()) return false;
|
||||
if (!this.isCompatibleLicense()) return false;
|
||||
if (!action.isCompatible) return true;
|
||||
return action.isCompatible(context);
|
||||
},
|
||||
execute: async (context: ActionContext): Promise<void> => {
|
||||
this.notifyFeatureUsage();
|
||||
return action.execute(context);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public supportedTriggers(): SupportedTriggers[] {
|
||||
return this.def.supportedTriggers();
|
||||
}
|
||||
|
||||
private notifyFeatureUsage(): void {
|
||||
if (!this.minimalLicense || !this.licenseFeatureName) return;
|
||||
this.deps
|
||||
.getFeatureUsageStart()
|
||||
.notifyUsage(this.licenseFeatureName)
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`ActionFactory [actionFactory.id = ${this.def.id}] fail notify feature usage.`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,11 +34,18 @@ export interface ActionFactoryDefinition<
|
|||
id: string;
|
||||
|
||||
/**
|
||||
* Minimal licence level
|
||||
* Empty means no licence restrictions
|
||||
* Minimal license level
|
||||
* Empty means no license restrictions
|
||||
*/
|
||||
readonly minimalLicense?: LicenseType;
|
||||
|
||||
/**
|
||||
* Required when `minimalLicense` is used.
|
||||
* Is a user-facing string. Has to be unique. Doesn't need i18n.
|
||||
* The feature's name will be displayed to Cloud end-users when they're billed based on their feature usage.
|
||||
*/
|
||||
licenseFeatureName?: string;
|
||||
|
||||
/**
|
||||
* This method should return a definition of a new action, normally used to
|
||||
* register it in `ui_actions` registry.
|
||||
|
|
|
@ -87,7 +87,9 @@ const setup = (
|
|||
actions,
|
||||
});
|
||||
const uiActionsEnhancements = new UiActionsServiceEnhancements({
|
||||
getLicenseInfo,
|
||||
getLicense: getLicenseInfo,
|
||||
featureUsageSetup: licensingMock.createSetup().featureUsage,
|
||||
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
|
||||
});
|
||||
const manager = new DynamicActionManager({
|
||||
isCompatible,
|
||||
|
@ -671,11 +673,13 @@ describe('DynamicActionManager', () => {
|
|||
const basicActionFactory: ActionFactoryDefinition = {
|
||||
...actionFactoryDefinition1,
|
||||
minimalLicense: 'basic',
|
||||
licenseFeatureName: 'Feature 1',
|
||||
};
|
||||
|
||||
const goldActionFactory: ActionFactoryDefinition = {
|
||||
...actionFactoryDefinition2,
|
||||
minimalLicense: 'gold',
|
||||
licenseFeatureName: 'Feature 2',
|
||||
};
|
||||
|
||||
uiActions.registerActionFactory(basicActionFactory);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { embeddablePluginMock } from '../../../../src/plugins/embeddable/public/
|
|||
import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '.';
|
||||
import { plugin as pluginInitializer } from '.';
|
||||
import { licensingMock } from '../../licensing/public/mocks';
|
||||
import { StartDependencies } from './plugin';
|
||||
|
||||
export type Setup = jest.Mocked<AdvancedUiActionsSetup>;
|
||||
export type Start = jest.Mocked<AdvancedUiActionsStart>;
|
||||
|
@ -35,7 +36,7 @@ const createStartContract = (): Start => {
|
|||
};
|
||||
|
||||
const createPlugin = (
|
||||
coreSetup: CoreSetup = coreMock.createSetup(),
|
||||
coreSetup: CoreSetup<StartDependencies> = coreMock.createSetup(),
|
||||
coreStart: CoreStart = coreMock.createStart()
|
||||
) => {
|
||||
const pluginInitializerContext = coreMock.createPluginInitializerContext();
|
||||
|
@ -47,6 +48,7 @@ const createPlugin = (
|
|||
const setup = plugin.setup(coreSetup, {
|
||||
uiActions: uiActions.setup,
|
||||
embeddable: embeddable.setup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -36,16 +36,17 @@ import {
|
|||
} from './custom_time_range_badge';
|
||||
import { CommonlyUsedRange } from './types';
|
||||
import { UiActionsServiceEnhancements } from './services';
|
||||
import { ILicense, LicensingPluginStart } from '../../licensing/public';
|
||||
import { ILicense, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public';
|
||||
import { createFlyoutManageDrilldowns } from './drilldowns';
|
||||
import { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
import { createStartServicesGetter, Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
interface SetupDependencies {
|
||||
embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions.
|
||||
uiActions: UiActionsSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
interface StartDependencies {
|
||||
export interface StartDependencies {
|
||||
embeddable: EmbeddableStart;
|
||||
uiActions: UiActionsStart;
|
||||
licensing: LicensingPluginStart;
|
||||
|
@ -70,23 +71,30 @@ declare module '../../../../src/plugins/ui_actions/public' {
|
|||
|
||||
export class AdvancedUiActionsPublicPlugin
|
||||
implements Plugin<SetupContract, StartContract, SetupDependencies, StartDependencies> {
|
||||
readonly licenceInfo = new BehaviorSubject<ILicense | undefined>(undefined);
|
||||
readonly licenseInfo = new BehaviorSubject<ILicense | undefined>(undefined);
|
||||
private getLicenseInfo(): ILicense {
|
||||
if (!this.licenceInfo.getValue()) {
|
||||
if (!this.licenseInfo.getValue()) {
|
||||
throw new Error(
|
||||
'AdvancedUiActionsPublicPlugin: Licence is not ready! Licence becomes available only after setup.'
|
||||
'AdvancedUiActionsPublicPlugin: License is not ready! License becomes available only after setup.'
|
||||
);
|
||||
}
|
||||
return this.licenceInfo.getValue()!;
|
||||
return this.licenseInfo.getValue()!;
|
||||
}
|
||||
private readonly enhancements = new UiActionsServiceEnhancements({
|
||||
getLicenseInfo: () => this.getLicenseInfo(),
|
||||
});
|
||||
private enhancements?: UiActionsServiceEnhancements;
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup, { uiActions }: SetupDependencies): SetupContract {
|
||||
public setup(
|
||||
core: CoreSetup<StartDependencies>,
|
||||
{ uiActions, licensing }: SetupDependencies
|
||||
): SetupContract {
|
||||
const startServices = createStartServicesGetter(core.getStartServices);
|
||||
this.enhancements = new UiActionsServiceEnhancements({
|
||||
getLicense: () => this.getLicenseInfo(),
|
||||
featureUsageSetup: licensing.featureUsage,
|
||||
getFeatureUsageStart: () => startServices().plugins.licensing.featureUsage,
|
||||
});
|
||||
return {
|
||||
...uiActions,
|
||||
...this.enhancements,
|
||||
|
@ -94,7 +102,7 @@ export class AdvancedUiActionsPublicPlugin
|
|||
}
|
||||
|
||||
public start(core: CoreStart, { uiActions, licensing }: StartDependencies): StartContract {
|
||||
this.subs.push(licensing.license$.subscribe(this.licenceInfo));
|
||||
this.subs.push(licensing.license$.subscribe(this.licenseInfo));
|
||||
|
||||
const dateFormat = core.uiSettings.get('dateFormat') as string;
|
||||
const commonlyUsedRanges = core.uiSettings.get(
|
||||
|
@ -117,9 +125,9 @@ export class AdvancedUiActionsPublicPlugin
|
|||
|
||||
return {
|
||||
...uiActions,
|
||||
...this.enhancements,
|
||||
...this.enhancements!,
|
||||
FlyoutManageDrilldowns: createFlyoutManageDrilldowns({
|
||||
actionFactories: this.enhancements.getActionFactories(),
|
||||
actionFactories: this.enhancements!.getActionFactories(),
|
||||
getTrigger: (triggerId: TriggerId) => uiActions.getTrigger(triggerId),
|
||||
storage: new Storage(window?.localStorage),
|
||||
toastService: core.notifications.toasts,
|
||||
|
|
|
@ -4,11 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UiActionsServiceEnhancements } from './ui_actions_service_enhancements';
|
||||
import {
|
||||
UiActionsServiceEnhancements,
|
||||
UiActionsServiceEnhancementsParams,
|
||||
} from './ui_actions_service_enhancements';
|
||||
import { ActionFactoryDefinition, ActionFactory } from '../dynamic_actions';
|
||||
import { licensingMock } from '../../../licensing/public/mocks';
|
||||
|
||||
const getLicenseInfo = () => licensingMock.createLicense();
|
||||
const deps: UiActionsServiceEnhancementsParams = {
|
||||
getLicense: () => licensingMock.createLicense(),
|
||||
featureUsageSetup: licensingMock.createSetup().featureUsage,
|
||||
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
|
||||
};
|
||||
|
||||
describe('UiActionsService', () => {
|
||||
describe('action factories', () => {
|
||||
|
@ -34,7 +41,7 @@ describe('UiActionsService', () => {
|
|||
};
|
||||
|
||||
test('.getActionFactories() returns empty array if no action factories registered', () => {
|
||||
const service = new UiActionsServiceEnhancements({ getLicenseInfo });
|
||||
const service = new UiActionsServiceEnhancements(deps);
|
||||
|
||||
const factories = service.getActionFactories();
|
||||
|
||||
|
@ -42,7 +49,7 @@ describe('UiActionsService', () => {
|
|||
});
|
||||
|
||||
test('can register and retrieve an action factory', () => {
|
||||
const service = new UiActionsServiceEnhancements({ getLicenseInfo });
|
||||
const service = new UiActionsServiceEnhancements(deps);
|
||||
|
||||
service.registerActionFactory(factoryDefinition1);
|
||||
|
||||
|
@ -53,7 +60,7 @@ describe('UiActionsService', () => {
|
|||
});
|
||||
|
||||
test('can retrieve all action factories', () => {
|
||||
const service = new UiActionsServiceEnhancements({ getLicenseInfo });
|
||||
const service = new UiActionsServiceEnhancements(deps);
|
||||
|
||||
service.registerActionFactory(factoryDefinition1);
|
||||
service.registerActionFactory(factoryDefinition2);
|
||||
|
@ -67,7 +74,7 @@ describe('UiActionsService', () => {
|
|||
});
|
||||
|
||||
test('throws when retrieving action factory that does not exist', () => {
|
||||
const service = new UiActionsServiceEnhancements({ getLicenseInfo });
|
||||
const service = new UiActionsServiceEnhancements(deps);
|
||||
|
||||
service.registerActionFactory(factoryDefinition1);
|
||||
|
||||
|
@ -77,7 +84,7 @@ describe('UiActionsService', () => {
|
|||
});
|
||||
|
||||
test('isCompatible from definition is used on registered factory', async () => {
|
||||
const service = new UiActionsServiceEnhancements({ getLicenseInfo });
|
||||
const service = new UiActionsServiceEnhancements(deps);
|
||||
|
||||
service.registerActionFactory({
|
||||
...factoryDefinition1,
|
||||
|
@ -88,5 +95,27 @@ describe('UiActionsService', () => {
|
|||
service.getActionFactory(factoryDefinition1.id).isCompatible({ triggers: [] })
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
|
||||
describe('registerFeature for licensing', () => {
|
||||
const spy = jest.spyOn(deps.featureUsageSetup, 'register');
|
||||
beforeEach(() => {
|
||||
spy.mockClear();
|
||||
});
|
||||
test('registerFeature is not called if no license requirements', () => {
|
||||
const service = new UiActionsServiceEnhancements(deps);
|
||||
service.registerActionFactory(factoryDefinition1);
|
||||
expect(spy).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('registerFeature is called if has license requirements', () => {
|
||||
const service = new UiActionsServiceEnhancements(deps);
|
||||
service.registerActionFactory({
|
||||
...factoryDefinition1,
|
||||
minimalLicense: 'gold',
|
||||
licenseFeatureName: 'a name',
|
||||
});
|
||||
expect(spy).toBeCalledWith('a name', 'gold');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,19 +13,22 @@ import {
|
|||
import { DrilldownDefinition } from '../drilldowns';
|
||||
import { ILicense } from '../../../licensing/common/types';
|
||||
import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public';
|
||||
import { LicensingPluginSetup, LicensingPluginStart } from '../../../licensing/public';
|
||||
|
||||
export interface UiActionsServiceEnhancementsParams {
|
||||
readonly actionFactories?: ActionFactoryRegistry;
|
||||
readonly getLicenseInfo: () => ILicense;
|
||||
readonly getLicense: () => ILicense;
|
||||
readonly featureUsageSetup: LicensingPluginSetup['featureUsage'];
|
||||
readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage'];
|
||||
}
|
||||
|
||||
export class UiActionsServiceEnhancements {
|
||||
protected readonly actionFactories: ActionFactoryRegistry;
|
||||
protected readonly getLicenseInfo: () => ILicense;
|
||||
protected readonly deps: Omit<UiActionsServiceEnhancementsParams, 'actionFactories'>;
|
||||
|
||||
constructor({ actionFactories = new Map(), getLicenseInfo }: UiActionsServiceEnhancementsParams) {
|
||||
constructor({ actionFactories = new Map(), ...deps }: UiActionsServiceEnhancementsParams) {
|
||||
this.actionFactories = actionFactories;
|
||||
this.getLicenseInfo = getLicenseInfo;
|
||||
this.deps = deps;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,9 +54,10 @@ export class UiActionsServiceEnhancements {
|
|||
SupportedTriggers,
|
||||
FactoryContext,
|
||||
ActionContext
|
||||
>(definition, this.getLicenseInfo);
|
||||
>(definition, this.deps);
|
||||
|
||||
this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory<any, any, any>);
|
||||
this.registerFeatureUsage(definition);
|
||||
};
|
||||
|
||||
public readonly getActionFactory = (actionFactoryId: string): ActionFactory => {
|
||||
|
@ -94,6 +98,7 @@ export class UiActionsServiceEnhancements {
|
|||
execute,
|
||||
getHref,
|
||||
minimalLicense,
|
||||
licenseFeatureName,
|
||||
supportedTriggers,
|
||||
isCompatible,
|
||||
}: DrilldownDefinition<Config, SupportedTriggers, FactoryContext, ExecutionContext>): void => {
|
||||
|
@ -105,6 +110,7 @@ export class UiActionsServiceEnhancements {
|
|||
> = {
|
||||
id: factoryId,
|
||||
minimalLicense,
|
||||
licenseFeatureName,
|
||||
order,
|
||||
CollectConfig,
|
||||
createConfig,
|
||||
|
@ -128,4 +134,19 @@ export class UiActionsServiceEnhancements {
|
|||
|
||||
this.registerActionFactory(actionFactory);
|
||||
};
|
||||
|
||||
private registerFeatureUsage = (definition: ActionFactoryDefinition<any, any, any>): void => {
|
||||
if (!definition.minimalLicense || !definition.licenseFeatureName) return;
|
||||
|
||||
// Intentionally don't wait for response because
|
||||
// happens in setup phase and has to be sync
|
||||
this.deps.featureUsageSetup
|
||||
.register(definition.licenseFeatureName, definition.minimalLicense)
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`ActionFactory [actionFactory.id = ${definition.id}] fail to register feature for featureUsage.`
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue