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 all 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
3 changes: 3 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,9 @@ import { Presentable } from '../util/presentable';
import { uiToReactComponent } from '../../../kibana_react/public';
import { ActionType } from '../types';

/**
* @internal
*/
export class ActionInternal<A extends ActionDefinition = ActionDefinition>
implements Action<Context<A>>, Presentable<Context<A>> {
constructor(public readonly definition: A) {}
Expand Down
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, ActionRegistry } from '../types';
import { Trigger } from '../triggers';

// Casting to ActionType or TriggerId is a hack - in a real situation use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ export class UiActionsService {
for (const [key, value] of this.actions.entries()) actions.set(key, value);
for (const [key, value] of this.triggerToActions.entries())
triggerToActions.set(key, [...value]);

return new UiActionsService({ triggers, actions, triggerToActions });
};
}
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 = () => {
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
Expand Up @@ -7,7 +7,15 @@
import React from 'react';
import { cleanup, fireEvent, render } from '@testing-library/react/pure';
import { TEST_SUBJ_ACTION_FACTORY_ITEM, TEST_SUBJ_SELECTED_ACTION_FACTORY } from './action_wizard';
import { dashboardFactory, dashboards, Demo, urlFactory } from './test_data';
import {
dashboardFactory,
dashboards,
Demo,
urlFactory,
urlDrilldownActionFactory,
} from './test_data';
import { ActionFactory } from '../../dynamic_actions';
import { licenseMock } from '../../../../licensing/common/licensing.mock';

// TODO: afterEach is not available for it globally during setup
// https://github.com/elastic/kibana/issues/59469
Expand Down Expand Up @@ -54,3 +62,19 @@ test('If only one actions factory is available then actionFactory selection is e
// check that can't change to action factory type
expect(screen.queryByTestId(/change/i)).not.toBeInTheDocument();
});

test('If not enough license, button is disabled', () => {
const urlWithGoldLicense = new ActionFactory(
{
...urlDrilldownActionFactory,
minimalLicense: 'gold',
},
() => licenseMock.createLicense()
);
const screen = render(<Demo actionFactories={[dashboardFactory, urlWithGoldLicense]} />);

// check that all factories are displayed to pick
expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2);

expect(screen.getByText(/Go to URL/i)).toBeDisabled();
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiKeyPadMenuItem,
EuiSpacer,
EuiText,
EuiKeyPadMenuItem,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { txtChangeButton } from './i18n';
import './action_wizard.scss';
import { ActionFactory } from '../../dynamic_actions';
Expand Down Expand Up @@ -61,7 +63,11 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
context,
}) => {
// auto pick action factory if there is only 1 available
if (!currentActionFactory && actionFactories.length === 1) {
if (
!currentActionFactory &&
actionFactories.length === 1 &&
actionFactories[0].isCompatibleLicence()
) {
onActionFactoryChange(actionFactories[0]);
}

Expand Down Expand Up @@ -175,24 +181,46 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
willChange: 'opacity',
};

/**
* 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());
return [
...compatibleLicense.sort((f1, f2) => f2.order - f1.order),
...notCompatibleLicense.sort((f1, f2) => f2.order - f1.order),
];
};

return (
<EuiFlexGroup gutterSize="m" wrap={true} style={firefoxBugFix}>
{[...actionFactories]
.sort((f1, f2) => f2.order - f1.order)
.map((actionFactory) => (
<EuiFlexItem grow={false} key={actionFactory.id}>
{ensureOrder(actionFactories).map((actionFactory) => (
<EuiFlexItem grow={false} key={actionFactory.id}>
<EuiToolTip
content={
!actionFactory.isCompatibleLicence() && (
<FormattedMessage
defaultMessage="Insufficient license level"
id="xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip"
/>
)
}
>
<EuiKeyPadMenuItem
className="auaActionWizard__actionFactoryItem"
label={actionFactory.getDisplayName(context)}
data-test-subj={`${TEST_SUBJ_ACTION_FACTORY_ITEM}-${actionFactory.id}`}
onClick={() => onActionFactorySelected(actionFactory)}
disabled={!actionFactory.isCompatibleLicence()}
>
{actionFactory.getIconType(context) && (
<EuiIcon type={actionFactory.getIconType(context)!} size="m" />
)}
</EuiKeyPadMenuItem>
</EuiFlexItem>
))}
</EuiToolTip>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/p
import { ActionWizard } from './action_wizard';
import { ActionFactoryDefinition, ActionFactory } from '../../dynamic_actions';
import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public';
import { licenseMock } from '../../../../licensing/common/licensing.mock';

type ActionBaseConfig = object;

Expand Down Expand Up @@ -101,10 +102,13 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition<
create: () => ({
id: 'test',
execute: async () => alert('Navigate to dashboard!'),
enhancements: {},
}),
};

export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory);
export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () =>
licenseMock.createLicense()
);

interface UrlDrilldownConfig {
url: string;
Expand Down Expand Up @@ -159,7 +163,9 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition<UrlDrilldownConf
create: () => null as any,
};

export const urlFactory = new ActionFactory(urlDrilldownActionFactory);
export const urlFactory = new ActionFactory(urlDrilldownActionFactory, () =>
licenseMock.createLicense()
);

export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory<any>> }) {
const [state, setState] = useState<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public';
import { DrilldownListItem } from '../list_manage_drilldowns';
import {
insufficientLicenseLevel,
invalidDrilldownType,
toastDrilldownCreated,
toastDrilldownDeleted,
toastDrilldownEdited,
Expand Down Expand Up @@ -133,6 +135,11 @@ export function createFlyoutManageDrilldowns({
drilldownName: drilldown.action.name,
actionName: actionFactory?.getDisplayName(factoryContext) ?? drilldown.action.factoryId,
icon: actionFactory?.getIconType(factoryContext),
error: !actionFactory
? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development
: !actionFactory.isCompatibleLicence()
? insufficientLicenseLevel
: undefined,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,23 @@ export const toastDrilldownsCRUDError = i18n.translate(
description: 'Title for generic error toast when persisting drilldown updates failed',
}
);

export const insufficientLicenseLevel = i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError',
{
defaultMessage: 'Insufficient license level',
description:
'User created drilldown with higher license type, but then downgraded the license. This error is shown in the list near created drilldown',
}
);

export const invalidDrilldownType = (type: string) =>
i18n.translate(
'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType',
{
defaultMessage: "Drilldown type {type} doesn't exist",
values: {
type,
},
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ storiesOf('components/FlyoutListManageDrilldowns', module).add('default', () =>
drilldowns={[
{ id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' },
{ id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' },
{ id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3' },
{ id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3', error: 'Some error...' },
]}
/>
</EuiFlyout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
*/

import React from 'react';
import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui';
import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n';
import { ActionFactory } from '../../../dynamic_actions';
import { ActionWizard } from '../../../components/action_wizard';

const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions';

const noopFn = () => {};

export interface FormDrilldownWizardProps {
Expand Down Expand Up @@ -49,10 +52,32 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
</EuiFormRow>
);

const hasNotCompatibleLicenseFactory = () =>
actionFactories?.some((f) => !f.isCompatibleLicence());

const renderGetMoreActionsLink = () => (
<EuiText size="s">
<EuiLink
href={GET_MORE_ACTIONS_LINK}
target="_blank"
external
data-test-subj={'getMoreActionsLink'}
>
<FormattedMessage
id="xpack.uiActionsEnhanced.drilldowns.components.FormDrilldownWizard.getMoreActionsLinkLabel"
defaultMessage="Get more actions"
/>
</EuiLink>
</EuiText>
);

const actionWizard = (
<EuiFormRow
label={actionFactories?.length > 1 ? txtDrilldownAction : undefined}
fullWidth={true}
labelAppend={
!currentActionFactory && hasNotCompatibleLicenseFactory() && renderGetMoreActionsLink()
}
>
<ActionWizard
actionFactories={actionFactories}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ afterEach(cleanup);
const drilldowns: DrilldownListItem[] = [
{ id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' },
{ id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' },
{ id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3' },
{ id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3', error: 'an error' },
];

test('Render list of drilldowns', () => {
Expand Down Expand Up @@ -67,3 +67,8 @@ test('Can delete drilldowns', () => {

expect(fn).toBeCalledWith([drilldowns[1].id, drilldowns[2].id]);
});

test('Error is displayed', () => {
const screen = render(<ListManageDrilldowns drilldowns={drilldowns} />);
expect(screen.getByLabelText('an error')).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
EuiIcon,
EuiSpacer,
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
import React, { useState } from 'react';
import {
Expand All @@ -28,6 +29,7 @@ export interface DrilldownListItem {
actionName: string;
drilldownName: string;
icon?: string;
error?: string;
}

export interface ListManageDrilldownsProps {
Expand All @@ -52,11 +54,27 @@ export function ListManageDrilldowns({

const columns: Array<EuiBasicTableColumn<DrilldownListItem>> = [
{
field: 'drilldownName',
name: 'Name',
truncateText: true,
width: '50%',
'data-test-subj': 'drilldownListItemName',
render: (drilldown: DrilldownListItem) => (
<div>
{drilldown.drilldownName}{' '}
{drilldown.error && (
<EuiToolTip id={`drilldownError-${drilldown.id}`} content={drilldown.error}>
<EuiIcon
type="alert"
color="danger"
title={drilldown.error}
aria-label={drilldown.error}
data-test-subj={`drilldownError-${drilldown.id}`}
style={{ marginLeft: '4px' }} /* a bit of spacing from text */
/>
</EuiToolTip>
)}
</div>
),
},
{
name: 'Action',
Expand Down
Loading