mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
parent
e251310000
commit
498abb4152
164 changed files with 897 additions and 5367 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -3,7 +3,6 @@
|
|||
# For more info, see https://help.github.com/articles/about-codeowners/
|
||||
|
||||
# App
|
||||
/x-pack/legacy/plugins/dashboard_enhanced/ @elastic/kibana-app
|
||||
/x-pack/legacy/plugins/lens/ @elastic/kibana-app
|
||||
/x-pack/legacy/plugins/graph/ @elastic/kibana-app
|
||||
/src/legacy/server/url_shortening/ @elastic/kibana-app
|
||||
|
|
|
@ -46,7 +46,7 @@ export class UiActionExamplesPlugin
|
|||
}));
|
||||
|
||||
uiActions.registerAction(helloWorldAction);
|
||||
uiActions.addTriggerAction(helloWorldTrigger.id, helloWorldAction);
|
||||
uiActions.attachAction(helloWorldTrigger.id, helloWorldAction);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
|
|
@ -95,7 +95,8 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => {
|
|||
);
|
||||
},
|
||||
});
|
||||
uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction);
|
||||
uiActionsApi.registerAction(dynamicAction);
|
||||
uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction);
|
||||
setConfirmationText(
|
||||
`You've successfully added a new action: ${dynamicAction.getDisplayName(
|
||||
{}
|
||||
|
|
|
@ -79,21 +79,21 @@ export class UiActionsExplorerPlugin implements Plugin<void, void, {}, StartDeps
|
|||
|
||||
const startServices = core.getStartServices();
|
||||
|
||||
deps.uiActions.addTriggerAction(
|
||||
deps.uiActions.attachAction(
|
||||
USER_TRIGGER,
|
||||
createPhoneUserAction(async () => (await startServices)[1].uiActions)
|
||||
);
|
||||
deps.uiActions.addTriggerAction(
|
||||
deps.uiActions.attachAction(
|
||||
USER_TRIGGER,
|
||||
createEditUserAction(async () => (await startServices)[0].overlays.openModal)
|
||||
);
|
||||
|
||||
deps.uiActions.addTriggerAction(COUNTRY_TRIGGER, viewInMapsAction);
|
||||
deps.uiActions.addTriggerAction(COUNTRY_TRIGGER, lookUpWeatherAction);
|
||||
deps.uiActions.addTriggerAction(COUNTRY_TRIGGER, showcasePluggability);
|
||||
deps.uiActions.addTriggerAction(PHONE_TRIGGER, makePhoneCallAction);
|
||||
deps.uiActions.addTriggerAction(PHONE_TRIGGER, showcasePluggability);
|
||||
deps.uiActions.addTriggerAction(USER_TRIGGER, showcasePluggability);
|
||||
deps.uiActions.attachAction(COUNTRY_TRIGGER, viewInMapsAction);
|
||||
deps.uiActions.attachAction(COUNTRY_TRIGGER, lookUpWeatherAction);
|
||||
deps.uiActions.attachAction(COUNTRY_TRIGGER, showcasePluggability);
|
||||
deps.uiActions.attachAction(PHONE_TRIGGER, makePhoneCallAction);
|
||||
deps.uiActions.attachAction(PHONE_TRIGGER, showcasePluggability);
|
||||
deps.uiActions.attachAction(USER_TRIGGER, showcasePluggability);
|
||||
|
||||
core.application.register({
|
||||
id: 'uiActionsExplorer',
|
||||
|
|
|
@ -91,7 +91,6 @@ export interface OverlayFlyoutStart {
|
|||
export interface OverlayFlyoutOpenOptions {
|
||||
className?: string;
|
||||
closeButtonAriaLabel?: string;
|
||||
ownFocus?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,14 +18,12 @@
|
|||
*/
|
||||
|
||||
export const storybookAliases = {
|
||||
advanced_ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js',
|
||||
apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js',
|
||||
canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js',
|
||||
codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts',
|
||||
dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/scripts/storybook.js',
|
||||
drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js',
|
||||
embeddable: 'src/plugins/embeddable/scripts/storybook.js',
|
||||
infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js',
|
||||
siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js',
|
||||
ui_actions: 'src/plugins/ui_actions/scripts/storybook.js',
|
||||
ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js',
|
||||
};
|
||||
|
|
|
@ -45,7 +45,6 @@ import { PersistedState } from '../../../../../../../plugins/visualizations/publ
|
|||
import { buildPipeline } from '../legacy/build_pipeline';
|
||||
import { Vis } from '../vis';
|
||||
import { getExpressions, getUiActions } from '../services';
|
||||
import { VisualizationsStartDeps } from '../plugin';
|
||||
import { VIS_EVENT_TO_TRIGGER } from './events';
|
||||
|
||||
const getKeys = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<keyof T>;
|
||||
|
@ -57,7 +56,6 @@ export interface VisualizeEmbeddableConfiguration {
|
|||
editable: boolean;
|
||||
appState?: { save(): void };
|
||||
uiState?: PersistedState;
|
||||
uiActions?: VisualizationsStartDeps['uiActions'];
|
||||
}
|
||||
|
||||
export interface VisualizeInput extends EmbeddableInput {
|
||||
|
@ -96,7 +94,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
|
||||
constructor(
|
||||
timefilter: TimefilterContract,
|
||||
{ vis, editUrl, indexPatterns, editable, uiActions }: VisualizeEmbeddableConfiguration,
|
||||
{ vis, editUrl, indexPatterns, editable }: VisualizeEmbeddableConfiguration,
|
||||
initialInput: VisualizeInput,
|
||||
parent?: Container
|
||||
) {
|
||||
|
@ -109,8 +107,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
editable,
|
||||
visTypeName: vis.type.name,
|
||||
},
|
||||
parent,
|
||||
{ uiActions }
|
||||
parent
|
||||
);
|
||||
this.timefilter = timefilter;
|
||||
this.vis = vis;
|
||||
|
@ -268,7 +265,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
timeFieldName: this.vis.data.indexPattern!.timeFieldName!,
|
||||
data: event.data,
|
||||
};
|
||||
|
||||
getUiActions()
|
||||
.getTrigger(triggerId)
|
||||
.exec(context);
|
||||
|
|
|
@ -38,7 +38,6 @@ import {
|
|||
getTimeFilter,
|
||||
} from '../services';
|
||||
import { showNewVisModal } from '../wizard';
|
||||
import { VisualizationsStartDeps } from '../plugin';
|
||||
import { convertToSerializedVis } from '../saved_visualizations/_saved_vis';
|
||||
|
||||
interface VisualizationAttributes extends SavedObjectAttributes {
|
||||
|
@ -53,11 +52,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
|
|||
> {
|
||||
public readonly type = VISUALIZE_EMBEDDABLE_TYPE;
|
||||
|
||||
constructor(
|
||||
private readonly getUiActions: () => Promise<
|
||||
Pick<VisualizationsStartDeps, 'uiActions'>['uiActions']
|
||||
>
|
||||
) {
|
||||
constructor() {
|
||||
super({
|
||||
savedObjectMetaData: {
|
||||
name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }),
|
||||
|
@ -119,8 +114,6 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
|
|||
|
||||
const indexPattern = vis.data.indexPattern;
|
||||
const indexPatterns = indexPattern ? [indexPattern] : [];
|
||||
const uiActions = await this.getUiActions();
|
||||
|
||||
const editable = await this.isEditable();
|
||||
return new VisualizeEmbeddable(
|
||||
getTimeFilter(),
|
||||
|
@ -131,7 +124,6 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
|
|||
editable,
|
||||
appState: input.appState,
|
||||
uiState: input.uiState,
|
||||
uiActions,
|
||||
},
|
||||
input,
|
||||
parent
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, PluginInitializerContext } from '../../../../../../core/public';
|
||||
import { PluginInitializerContext } from '../../../../../../core/public';
|
||||
import { VisualizationsSetup, VisualizationsStart } from './';
|
||||
import { VisualizationsPlugin } from './plugin';
|
||||
import { coreMock } from '../../../../../../core/public/mocks';
|
||||
|
@ -26,7 +26,6 @@ import { expressionsPluginMock } from '../../../../../../plugins/expressions/pub
|
|||
import { dataPluginMock } from '../../../../../../plugins/data/public/mocks';
|
||||
import { usageCollectionPluginMock } from '../../../../../../plugins/usage_collection/public/mocks';
|
||||
import { uiActionsPluginMock } from '../../../../../../plugins/ui_actions/public/mocks';
|
||||
import { VisualizationsStartDeps } from './plugin';
|
||||
|
||||
const createSetupContract = (): VisualizationsSetup => ({
|
||||
createBaseVisualization: jest.fn(),
|
||||
|
@ -49,7 +48,7 @@ const createStartContract = (): VisualizationsStart => ({
|
|||
const createInstance = async () => {
|
||||
const plugin = new VisualizationsPlugin({} as PluginInitializerContext);
|
||||
|
||||
const setup = plugin.setup(coreMock.createSetup() as CoreSetup<VisualizationsStartDeps>, {
|
||||
const setup = plugin.setup(coreMock.createSetup(), {
|
||||
data: dataPluginMock.createSetupContract(),
|
||||
expressions: expressionsPluginMock.createSetupContract(),
|
||||
embeddable: embeddablePluginMock.createSetupContract(),
|
||||
|
|
|
@ -111,7 +111,7 @@ export class VisualizationsPlugin
|
|||
constructor(initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<VisualizationsStartDeps>,
|
||||
core: CoreSetup,
|
||||
{ expressions, embeddable, usageCollection, data }: VisualizationsSetupDeps
|
||||
): VisualizationsSetup {
|
||||
setUISettings(core.uiSettings);
|
||||
|
@ -120,9 +120,7 @@ export class VisualizationsPlugin
|
|||
expressions.registerFunction(visualizationFunction);
|
||||
expressions.registerRenderer(visualizationRenderer);
|
||||
|
||||
const embeddableFactory = new VisualizeEmbeddableFactory(
|
||||
async () => (await core.getStartServices())[1].uiActions
|
||||
);
|
||||
const embeddableFactory = new VisualizeEmbeddableFactory();
|
||||
embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory);
|
||||
|
||||
return {
|
||||
|
|
|
@ -37,7 +37,7 @@ export interface ReplacePanelActionContext {
|
|||
export class ReplacePanelAction implements ActionByType<typeof ACTION_REPLACE_PANEL> {
|
||||
public readonly type = ACTION_REPLACE_PANEL;
|
||||
public readonly id = ACTION_REPLACE_PANEL;
|
||||
public order = 3;
|
||||
public order = 11;
|
||||
|
||||
constructor(
|
||||
private core: CoreStart,
|
||||
|
|
|
@ -87,7 +87,7 @@ export class DashboardEmbeddableContainerPublicPlugin
|
|||
): Setup {
|
||||
const expandPanelAction = new ExpandPanelAction();
|
||||
uiActions.registerAction(expandPanelAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction);
|
||||
const startServices = core.getStartServices();
|
||||
|
||||
if (share) {
|
||||
|
@ -146,7 +146,7 @@ export class DashboardEmbeddableContainerPublicPlugin
|
|||
plugins.embeddable.getEmbeddableFactories
|
||||
);
|
||||
uiActions.registerAction(changeViewAction);
|
||||
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, changeViewAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction);
|
||||
const savedDashboardLoader = createSavedDashboardLoader({
|
||||
savedObjectsClient: core.savedObjects.client,
|
||||
indexPatterns,
|
||||
|
|
|
@ -49,7 +49,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
|||
|
||||
const editModeAction = createEditModeAction();
|
||||
uiActionsSetup.registerAction(editModeAction);
|
||||
uiActionsSetup.addTriggerAction(CONTEXT_MENU_TRIGGER, editModeAction);
|
||||
uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction);
|
||||
setup.registerEmbeddableFactory(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
|
||||
|
|
|
@ -109,12 +109,12 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
|
|||
createFilterAction(queryService.filterManager, queryService.timefilter.timefilter)
|
||||
);
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
uiActions.attachAction(
|
||||
SELECT_RANGE_TRIGGER,
|
||||
selectRangeAction(queryService.filterManager, queryService.timefilter.timefilter)
|
||||
);
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
uiActions.attachAction(
|
||||
VALUE_CLICK_TRIGGER,
|
||||
valueClickAction(queryService.filterManager, queryService.timefilter.timefilter)
|
||||
);
|
||||
|
@ -146,10 +146,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
|
|||
const search = this.searchService.start(core);
|
||||
setSearchService(search);
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
APPLY_FILTER_TRIGGER,
|
||||
uiActions.getAction(ACTION_GLOBAL_APPLY_FILTER)
|
||||
);
|
||||
uiActions.attachAction(APPLY_FILTER_TRIGGER, uiActions.getAction(ACTION_GLOBAL_APPLY_FILTER));
|
||||
|
||||
const dataServices = {
|
||||
actions: {
|
||||
|
|
|
@ -33,7 +33,7 @@ interface ActionContext {
|
|||
export class EditPanelAction implements Action<ActionContext> {
|
||||
public readonly type = ACTION_EDIT_PANEL;
|
||||
public readonly id = ACTION_EDIT_PANEL;
|
||||
public order = 50;
|
||||
public order = 15;
|
||||
|
||||
constructor(private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']) {}
|
||||
|
||||
|
|
|
@ -16,35 +16,23 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { isEqual, cloneDeep } from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { Adapters, ViewMode } from '../types';
|
||||
import { Adapters } from '../types';
|
||||
import { IContainer } from '../containers';
|
||||
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
|
||||
import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable';
|
||||
import { ViewMode } from '../types';
|
||||
import { TriggerContextMapping } from '../ui_actions';
|
||||
import { EmbeddableActionStorage } from './embeddable_action_storage';
|
||||
import {
|
||||
UiActionsDynamicActionManager,
|
||||
UiActionsStart,
|
||||
} from '../../../../../plugins/ui_actions/public';
|
||||
import { EmbeddableContext } from '../triggers';
|
||||
|
||||
function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
|
||||
return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title;
|
||||
}
|
||||
|
||||
export interface EmbeddableParams {
|
||||
uiActions?: UiActionsStart;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -60,34 +48,15 @@ export abstract class Embeddable<
|
|||
// to update input when the parent changes.
|
||||
private parentSubscription?: Rx.Subscription;
|
||||
|
||||
private storageSubscription?: Rx.Subscription;
|
||||
|
||||
// TODO: Rename to destroyed.
|
||||
private destoyed: boolean = false;
|
||||
|
||||
private storage = new EmbeddableActionStorage((this as unknown) as Embeddable);
|
||||
|
||||
private cachedDynamicActions?: UiActionsDynamicActionManager;
|
||||
public get dynamicActions(): UiActionsDynamicActionManager | undefined {
|
||||
if (!this.params.uiActions) return undefined;
|
||||
if (!this.cachedDynamicActions) {
|
||||
this.cachedDynamicActions = new UiActionsDynamicActionManager({
|
||||
isCompatible: async (context: unknown) =>
|
||||
(context as EmbeddableContext).embeddable.runtimeId === this.runtimeId,
|
||||
storage: this.storage,
|
||||
uiActions: this.params.uiActions,
|
||||
});
|
||||
private __actionStorage?: EmbeddableActionStorage;
|
||||
public get actionStorage(): EmbeddableActionStorage {
|
||||
return this.__actionStorage || (this.__actionStorage = new EmbeddableActionStorage(this));
|
||||
}
|
||||
|
||||
return this.cachedDynamicActions;
|
||||
}
|
||||
|
||||
constructor(
|
||||
input: TEmbeddableInput,
|
||||
output: TEmbeddableOutput,
|
||||
parent?: IContainer,
|
||||
public readonly params: EmbeddableParams = {}
|
||||
) {
|
||||
constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) {
|
||||
this.id = input.id;
|
||||
this.output = {
|
||||
title: getPanelTitle(input, output),
|
||||
|
@ -111,18 +80,6 @@ export abstract class Embeddable<
|
|||
this.onResetInput(newInput);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.dynamicActions) {
|
||||
this.dynamicActions.start().catch(error => {
|
||||
/* eslint-disable */
|
||||
console.log('Failed to start embeddable dynamic actions', this);
|
||||
console.error(error);
|
||||
/* eslint-enable */
|
||||
});
|
||||
this.storageSubscription = this.input$.subscribe(() => {
|
||||
this.storage.reload$.next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getIsContainer(): this is IContainer {
|
||||
|
@ -201,20 +158,6 @@ 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.storageSubscription) {
|
||||
this.storageSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
if (this.parentSubscription) {
|
||||
this.parentSubscription.unsubscribe();
|
||||
}
|
||||
|
|
|
@ -20,8 +20,7 @@
|
|||
import { Embeddable } from './embeddable';
|
||||
import { EmbeddableInput } from './i_embeddable';
|
||||
import { ViewMode } from '../types';
|
||||
import { EmbeddableActionStorage } from './embeddable_action_storage';
|
||||
import { UiActionsSerializedEvent } from '../../../../ui_actions/public';
|
||||
import { EmbeddableActionStorage, SerializedEvent } from './embeddable_action_storage';
|
||||
import { of } from '../../../../kibana_utils/common';
|
||||
|
||||
class TestEmbeddable extends Embeddable<EmbeddableInput> {
|
||||
|
@ -43,9 +42,9 @@ describe('EmbeddableActionStorage', () => {
|
|||
test('can add event to embeddable', async () => {
|
||||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
const event: UiActionsSerializedEvent = {
|
||||
const event: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -58,40 +57,23 @@ describe('EmbeddableActionStorage', () => {
|
|||
expect(events2).toEqual([event]);
|
||||
});
|
||||
|
||||
test('does not merge .getInput() into .updateInput()', async () => {
|
||||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
const event: UiActionsSerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(embeddable, 'updateInput');
|
||||
|
||||
await storage.create(event);
|
||||
|
||||
expect(spy.mock.calls[0][0].id).toBe(undefined);
|
||||
expect(spy.mock.calls[0][0].viewMode).toBe(undefined);
|
||||
});
|
||||
|
||||
test('can create multiple events', async () => {
|
||||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event1: UiActionsSerializedEvent = {
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID1',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
const event2: UiActionsSerializedEvent = {
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
const event3: UiActionsSerializedEvent = {
|
||||
const event3: SerializedEvent = {
|
||||
eventId: 'EVENT_ID3',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -113,9 +95,9 @@ describe('EmbeddableActionStorage', () => {
|
|||
test('throws when creating an event with the same ID', async () => {
|
||||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
const event: UiActionsSerializedEvent = {
|
||||
const event: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -140,16 +122,16 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event1: UiActionsSerializedEvent = {
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'foo',
|
||||
} as any,
|
||||
};
|
||||
const event2: UiActionsSerializedEvent = {
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'bar',
|
||||
} as any,
|
||||
|
@ -166,30 +148,30 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event1: UiActionsSerializedEvent = {
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID1',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'foo',
|
||||
} as any,
|
||||
};
|
||||
const event2: UiActionsSerializedEvent = {
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'bar',
|
||||
} as any,
|
||||
};
|
||||
const event22: UiActionsSerializedEvent = {
|
||||
const event22: SerializedEvent = {
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'baz',
|
||||
} as any,
|
||||
};
|
||||
const event3: UiActionsSerializedEvent = {
|
||||
const event3: SerializedEvent = {
|
||||
eventId: 'EVENT_ID3',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'qux',
|
||||
} as any,
|
||||
|
@ -217,9 +199,9 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event: UiActionsSerializedEvent = {
|
||||
const event: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -235,14 +217,14 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event1: UiActionsSerializedEvent = {
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID1',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
const event2: UiActionsSerializedEvent = {
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -267,9 +249,9 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event: UiActionsSerializedEvent = {
|
||||
const event: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -284,23 +266,23 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event1: UiActionsSerializedEvent = {
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID1',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'foo',
|
||||
} as any,
|
||||
};
|
||||
const event2: UiActionsSerializedEvent = {
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'bar',
|
||||
} as any,
|
||||
};
|
||||
const event3: UiActionsSerializedEvent = {
|
||||
const event3: SerializedEvent = {
|
||||
eventId: 'EVENT_ID3',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {
|
||||
name: 'qux',
|
||||
} as any,
|
||||
|
@ -345,9 +327,9 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event: UiActionsSerializedEvent = {
|
||||
const event: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -373,9 +355,9 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event: UiActionsSerializedEvent = {
|
||||
const event: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -401,9 +383,9 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event: UiActionsSerializedEvent = {
|
||||
const event: SerializedEvent = {
|
||||
eventId: 'EVENT_ID',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -420,19 +402,19 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event1: UiActionsSerializedEvent = {
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID1',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID1',
|
||||
action: {} as any,
|
||||
};
|
||||
const event2: UiActionsSerializedEvent = {
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID2',
|
||||
action: {} as any,
|
||||
};
|
||||
const event3: UiActionsSerializedEvent = {
|
||||
const event3: SerializedEvent = {
|
||||
eventId: 'EVENT_ID3',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID3',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
@ -476,7 +458,7 @@ describe('EmbeddableActionStorage', () => {
|
|||
|
||||
await storage.create({
|
||||
eventId: 'EVENT_ID1',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID1',
|
||||
action: {} as any,
|
||||
});
|
||||
|
||||
|
@ -484,7 +466,7 @@ describe('EmbeddableActionStorage', () => {
|
|||
|
||||
await storage.create({
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID1',
|
||||
action: {} as any,
|
||||
});
|
||||
|
||||
|
@ -520,15 +502,15 @@ describe('EmbeddableActionStorage', () => {
|
|||
const embeddable = new TestEmbeddable();
|
||||
const storage = new EmbeddableActionStorage(embeddable);
|
||||
|
||||
const event1: UiActionsSerializedEvent = {
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID1',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID1',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
const event2: UiActionsSerializedEvent = {
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID2',
|
||||
triggers: ['TRIGGER-ID'],
|
||||
triggerId: 'TRIGGER-ID1',
|
||||
action: {} as any,
|
||||
};
|
||||
|
||||
|
|
|
@ -17,20 +17,32 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
UiActionsAbstractActionStorage,
|
||||
UiActionsSerializedEvent,
|
||||
} from '../../../../ui_actions/public';
|
||||
import { Embeddable } from '..';
|
||||
|
||||
export class EmbeddableActionStorage extends UiActionsAbstractActionStorage {
|
||||
constructor(private readonly embbeddable: Embeddable) {
|
||||
super();
|
||||
/**
|
||||
* Below two interfaces are here temporarily, they will move to `ui_actions`
|
||||
* plugin once #58216 is merged.
|
||||
*/
|
||||
export interface SerializedEvent {
|
||||
eventId: string;
|
||||
triggerId: string;
|
||||
action: unknown;
|
||||
}
|
||||
export interface ActionStorage {
|
||||
create(event: SerializedEvent): Promise<void>;
|
||||
update(event: SerializedEvent): Promise<void>;
|
||||
remove(eventId: string): Promise<void>;
|
||||
read(eventId: string): Promise<SerializedEvent>;
|
||||
count(): Promise<number>;
|
||||
list(): Promise<SerializedEvent[]>;
|
||||
}
|
||||
|
||||
async create(event: UiActionsSerializedEvent) {
|
||||
export class EmbeddableActionStorage implements ActionStorage {
|
||||
constructor(private readonly embbeddable: Embeddable<any, any>) {}
|
||||
|
||||
async create(event: SerializedEvent) {
|
||||
const input = this.embbeddable.getInput();
|
||||
const events = (input.events || []) as UiActionsSerializedEvent[];
|
||||
const events = (input.events || []) as SerializedEvent[];
|
||||
const exists = !!events.find(({ eventId }) => eventId === event.eventId);
|
||||
|
||||
if (exists) {
|
||||
|
@ -41,13 +53,14 @@ export class EmbeddableActionStorage extends UiActionsAbstractActionStorage {
|
|||
}
|
||||
|
||||
this.embbeddable.updateInput({
|
||||
...input,
|
||||
events: [...events, event],
|
||||
});
|
||||
}
|
||||
|
||||
async update(event: UiActionsSerializedEvent) {
|
||||
async update(event: SerializedEvent) {
|
||||
const input = this.embbeddable.getInput();
|
||||
const events = (input.events || []) as UiActionsSerializedEvent[];
|
||||
const events = (input.events || []) as SerializedEvent[];
|
||||
const index = events.findIndex(({ eventId }) => eventId === event.eventId);
|
||||
|
||||
if (index === -1) {
|
||||
|
@ -59,13 +72,14 @@ export class EmbeddableActionStorage extends UiActionsAbstractActionStorage {
|
|||
}
|
||||
|
||||
this.embbeddable.updateInput({
|
||||
...input,
|
||||
events: [...events.slice(0, index), event, ...events.slice(index + 1)],
|
||||
});
|
||||
}
|
||||
|
||||
async remove(eventId: string) {
|
||||
const input = this.embbeddable.getInput();
|
||||
const events = (input.events || []) as UiActionsSerializedEvent[];
|
||||
const events = (input.events || []) as SerializedEvent[];
|
||||
const index = events.findIndex(event => eventId === event.eventId);
|
||||
|
||||
if (index === -1) {
|
||||
|
@ -77,13 +91,14 @@ export class EmbeddableActionStorage extends UiActionsAbstractActionStorage {
|
|||
}
|
||||
|
||||
this.embbeddable.updateInput({
|
||||
...input,
|
||||
events: [...events.slice(0, index), ...events.slice(index + 1)],
|
||||
});
|
||||
}
|
||||
|
||||
async read(eventId: string): Promise<UiActionsSerializedEvent> {
|
||||
async read(eventId: string): Promise<SerializedEvent> {
|
||||
const input = this.embbeddable.getInput();
|
||||
const events = (input.events || []) as UiActionsSerializedEvent[];
|
||||
const events = (input.events || []) as SerializedEvent[];
|
||||
const event = events.find(ev => eventId === ev.eventId);
|
||||
|
||||
if (!event) {
|
||||
|
@ -98,10 +113,14 @@ export class EmbeddableActionStorage extends UiActionsAbstractActionStorage {
|
|||
|
||||
private __list() {
|
||||
const input = this.embbeddable.getInput();
|
||||
return (input.events || []) as UiActionsSerializedEvent[];
|
||||
return (input.events || []) as SerializedEvent[];
|
||||
}
|
||||
|
||||
async list(): Promise<UiActionsSerializedEvent[]> {
|
||||
async count(): Promise<number> {
|
||||
return this.__list().length;
|
||||
}
|
||||
|
||||
async list(): Promise<SerializedEvent[]> {
|
||||
return this.__list();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
|
@ -34,7 +33,7 @@ export interface EmbeddableInput {
|
|||
/**
|
||||
* Reserved key for `ui_actions` events.
|
||||
*/
|
||||
events?: Array<{ eventId: string }>;
|
||||
events?: unknown;
|
||||
|
||||
/**
|
||||
* List of action IDs that this embeddable should not render.
|
||||
|
@ -83,19 +82,6 @@ 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.
|
||||
*/
|
||||
dynamicActions?: UiActionsDynamicActionManager;
|
||||
|
||||
/**
|
||||
* A functional representation of the isContainer variable, but helpful for typescript to
|
||||
* know the shape if this returns true
|
||||
|
|
|
@ -44,7 +44,7 @@ import {
|
|||
import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
|
||||
const actionRegistry = new Map<string, Action>();
|
||||
const actionRegistry = new Map<string, Action<object | undefined | string | number>>();
|
||||
const triggerRegistry = new Map<string, Trigger>();
|
||||
const embeddableFactories = new Map<string, EmbeddableFactory>();
|
||||
const getEmbeddableFactory = (id: string) => embeddableFactories.get(id);
|
||||
|
@ -213,17 +213,13 @@ const renderInEditModeAndOpenContextMenu = async (
|
|||
};
|
||||
|
||||
test('HelloWorldContainer in edit mode hides disabledActions', async () => {
|
||||
const action = {
|
||||
const action: Action = {
|
||||
id: 'FOO',
|
||||
type: 'FOO' as ActionType,
|
||||
getIconType: () => undefined,
|
||||
getDisplayName: () => 'foo',
|
||||
isCompatible: async () => true,
|
||||
execute: async () => {},
|
||||
order: 10,
|
||||
getHref: () => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
const getActions = () => Promise.resolve([action]);
|
||||
|
||||
|
@ -249,17 +245,13 @@ test('HelloWorldContainer in edit mode hides disabledActions', async () => {
|
|||
});
|
||||
|
||||
test('HelloWorldContainer hides disabled badges', async () => {
|
||||
const action = {
|
||||
const action: Action = {
|
||||
id: 'BAR',
|
||||
type: 'BAR' as ActionType,
|
||||
getIconType: () => undefined,
|
||||
getDisplayName: () => 'bar',
|
||||
isCompatible: async () => true,
|
||||
execute: async () => {},
|
||||
order: 10,
|
||||
getHref: () => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
const getActions = () => Promise.resolve([action]);
|
||||
|
||||
|
|
|
@ -38,14 +38,6 @@ import { EditPanelAction } from '../actions';
|
|||
import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal';
|
||||
import { EmbeddableStart } from '../../plugin';
|
||||
|
||||
const sortByOrderField = (
|
||||
{ order: orderA }: { order?: number },
|
||||
{ order: orderB }: { order?: number }
|
||||
) => (orderB || 0) - (orderA || 0);
|
||||
|
||||
const removeById = (disabledActions: string[]) => ({ id }: { id: string }) =>
|
||||
disabledActions.indexOf(id) === -1;
|
||||
|
||||
interface Props {
|
||||
embeddable: IEmbeddable<any, any>;
|
||||
getActions: UiActionsService['getTriggerCompatibleActions'];
|
||||
|
@ -65,14 +57,12 @@ interface State {
|
|||
hidePanelTitles: boolean;
|
||||
closeContextMenu: boolean;
|
||||
badges: Array<Action<EmbeddableContext>>;
|
||||
eventCount?: number;
|
||||
}
|
||||
|
||||
export class EmbeddablePanel extends React.Component<Props, State> {
|
||||
private embeddableRoot: React.RefObject<HTMLDivElement>;
|
||||
private parentSubscription?: Subscription;
|
||||
private subscription?: Subscription;
|
||||
private eventCountSubscription?: Subscription;
|
||||
private mounted: boolean = false;
|
||||
private generateId = htmlIdGenerator();
|
||||
|
||||
|
@ -146,9 +136,6 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
if (this.eventCountSubscription) {
|
||||
this.eventCountSubscription.unsubscribe();
|
||||
}
|
||||
if (this.parentSubscription) {
|
||||
this.parentSubscription.unsubscribe();
|
||||
}
|
||||
|
@ -190,7 +177,6 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
badges={this.state.badges}
|
||||
embeddable={this.props.embeddable}
|
||||
headerId={headerId}
|
||||
eventCount={this.state.eventCount}
|
||||
/>
|
||||
)}
|
||||
<div className="embPanel__content" ref={this.embeddableRoot} />
|
||||
|
@ -202,15 +188,6 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
if (this.embeddableRoot.current) {
|
||||
this.props.embeddable.render(this.embeddableRoot.current);
|
||||
}
|
||||
|
||||
const dynamicActions = this.props.embeddable.dynamicActions;
|
||||
if (dynamicActions) {
|
||||
this.setState({ eventCount: dynamicActions.state.get().events.length });
|
||||
this.eventCountSubscription = dynamicActions.state.state$.subscribe(({ events }) => {
|
||||
if (!this.mounted) return;
|
||||
this.setState({ eventCount: events.length });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
closeMyContextMenuPanel = () => {
|
||||
|
@ -224,14 +201,13 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
private getActionContextMenuPanel = async () => {
|
||||
let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, {
|
||||
let actions = await this.props.getActions(CONTEXT_MENU_TRIGGER, {
|
||||
embeddable: this.props.embeddable,
|
||||
});
|
||||
|
||||
const { disabledActions } = this.props.embeddable.getInput();
|
||||
if (disabledActions) {
|
||||
const removeDisabledActions = removeById(disabledActions);
|
||||
regularActions = regularActions.filter(removeDisabledActions);
|
||||
actions = actions.filter(action => disabledActions.indexOf(action.id) === -1);
|
||||
}
|
||||
|
||||
const createGetUserData = (overlays: OverlayStart) =>
|
||||
|
@ -270,10 +246,16 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
new EditPanelAction(this.props.getEmbeddableFactory),
|
||||
];
|
||||
|
||||
const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField);
|
||||
const sorted = actions
|
||||
.concat(extraActions)
|
||||
.sort((a: Action<EmbeddableContext>, b: Action<EmbeddableContext>) => {
|
||||
const bOrder = b.order || 0;
|
||||
const aOrder = a.order || 0;
|
||||
return bOrder - aOrder;
|
||||
});
|
||||
|
||||
return await buildContextMenuForActions({
|
||||
actions: sortedActions,
|
||||
actions: sorted,
|
||||
actionContext: { embeddable: this.props.embeddable },
|
||||
closeMenu: this.closeMyContextMenuPanel,
|
||||
});
|
||||
|
|
|
@ -33,13 +33,15 @@ interface ActionContext {
|
|||
export class CustomizePanelTitleAction implements Action<ActionContext> {
|
||||
public readonly type = ACTION_CUSTOMIZE_PANEL;
|
||||
public id = ACTION_CUSTOMIZE_PANEL;
|
||||
public order = 40;
|
||||
public order = 10;
|
||||
|
||||
constructor(private readonly getDataFromUser: GetUserData) {}
|
||||
constructor(private readonly getDataFromUser: GetUserData) {
|
||||
this.order = 10;
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('embeddableApi.customizePanel.action.displayName', {
|
||||
defaultMessage: 'Edit panel title',
|
||||
defaultMessage: 'Customize panel',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ interface ActionContext {
|
|||
export class InspectPanelAction implements Action<ActionContext> {
|
||||
public readonly type = ACTION_INSPECT_PANEL;
|
||||
public readonly id = ACTION_INSPECT_PANEL;
|
||||
public order = 20;
|
||||
public order = 10;
|
||||
|
||||
constructor(private readonly inspector: InspectorStartContract) {}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ function hasExpandedPanelInput(
|
|||
export class RemovePanelAction implements Action<ActionContext> {
|
||||
public readonly type = REMOVE_PANEL_ACTION;
|
||||
public readonly id = REMOVE_PANEL_ACTION;
|
||||
public order = 1;
|
||||
public order = 5;
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
EuiIcon,
|
||||
EuiToolTip,
|
||||
EuiScreenReaderOnly,
|
||||
EuiNotificationBadge,
|
||||
} from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
@ -41,7 +40,6 @@ export interface PanelHeaderProps {
|
|||
badges: Array<Action<EmbeddableContext>>;
|
||||
embeddable: IEmbeddable;
|
||||
headerId?: string;
|
||||
eventCount?: number;
|
||||
}
|
||||
|
||||
function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmbeddable) {
|
||||
|
@ -92,7 +90,6 @@ export function PanelHeader({
|
|||
badges,
|
||||
embeddable,
|
||||
headerId,
|
||||
eventCount,
|
||||
}: PanelHeaderProps) {
|
||||
const viewDescription = getViewDescription(embeddable);
|
||||
const showTitle = !isViewMode || (title && !hidePanelTitles) || viewDescription !== '';
|
||||
|
@ -150,11 +147,7 @@ export function PanelHeader({
|
|||
)}
|
||||
{renderBadges(badges, embeddable)}
|
||||
</h2>
|
||||
{!isViewMode && !!eventCount && (
|
||||
<EuiNotificationBadge style={{ marginTop: '4px', marginRight: '4px' }}>
|
||||
{eventCount}
|
||||
</EuiNotificationBadge>
|
||||
)}
|
||||
|
||||
<PanelOptionsMenu
|
||||
isViewMode={isViewMode}
|
||||
getActionContextMenuPanel={getActionContextMenuPanel}
|
||||
|
|
|
@ -24,31 +24,24 @@ import { Comparator, Connect, StateContainer, UnboxState } from './types';
|
|||
|
||||
const { useContext, useLayoutEffect, useRef, createElement: h } = React;
|
||||
|
||||
/**
|
||||
* Returns the latest state of a state container.
|
||||
*
|
||||
* @param container State container which state to track.
|
||||
*/
|
||||
export const useContainerState = <Container extends StateContainer<any, any>>(
|
||||
container: Container
|
||||
): UnboxState<Container> => useObservable(container.state$, container.get());
|
||||
export const createStateContainerReactHelpers = <Container extends StateContainer<any, any>>() => {
|
||||
const context = React.createContext<Container>(null as any);
|
||||
|
||||
/**
|
||||
* Apply selector to state container to extract only needed information. Will
|
||||
* re-render your component only when the section changes.
|
||||
*
|
||||
* @param container State container which state to track.
|
||||
* @param selector Function used to pick parts of state.
|
||||
* @param comparator Comparator function used to memoize previous result, to not
|
||||
* re-render React component if state did not change. By default uses
|
||||
* `fast-deep-equal` package.
|
||||
*/
|
||||
export const useContainerSelector = <Container extends StateContainer<any, any>, Result>(
|
||||
container: Container,
|
||||
const useContainer = (): Container => useContext(context);
|
||||
|
||||
const useState = (): UnboxState<Container> => {
|
||||
const { state$, get } = useContainer();
|
||||
const value = useObservable(state$, get());
|
||||
return value;
|
||||
};
|
||||
|
||||
const useTransitions: () => Container['transitions'] = () => useContainer().transitions;
|
||||
|
||||
const useSelector = <Result>(
|
||||
selector: (state: UnboxState<Container>) => Result,
|
||||
comparator: Comparator<Result> = defaultComparator
|
||||
): Result => {
|
||||
const { state$, get } = container;
|
||||
const { state$, get } = useContainer();
|
||||
const lastValueRef = useRef<Result>(get());
|
||||
const [value, setValue] = React.useState<Result>(() => {
|
||||
const newValue = selector(get());
|
||||
|
@ -68,26 +61,6 @@ export const useContainerSelector = <Container extends StateContainer<any, any>,
|
|||
return value;
|
||||
};
|
||||
|
||||
export const createStateContainerReactHelpers = <Container extends StateContainer<any, any>>() => {
|
||||
const context = React.createContext<Container>(null as any);
|
||||
|
||||
const useContainer = (): Container => useContext(context);
|
||||
|
||||
const useState = (): UnboxState<Container> => {
|
||||
const container = useContainer();
|
||||
return useContainerState(container);
|
||||
};
|
||||
|
||||
const useTransitions: () => Container['transitions'] = () => useContainer().transitions;
|
||||
|
||||
const useSelector = <Result>(
|
||||
selector: (state: UnboxState<Container>) => Result,
|
||||
comparator: Comparator<Result> = defaultComparator
|
||||
): Result => {
|
||||
const container = useContainer();
|
||||
return useContainerSelector<Container, Result>(container, selector, comparator);
|
||||
};
|
||||
|
||||
const connect: Connect<UnboxState<Container>> = mapStateToProp => component => props =>
|
||||
h(component, { ...useSelector(mapStateToProp), ...props } as any);
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export interface BaseStateContainer<State extends BaseState> {
|
|||
|
||||
export interface StateContainer<
|
||||
State extends BaseState,
|
||||
PureTransitions extends object = object,
|
||||
PureTransitions extends object,
|
||||
PureSelectors extends object = {}
|
||||
> extends BaseStateContainer<State> {
|
||||
transitions: Readonly<PureTransitionsToTransitions<PureTransitions>>;
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { createStateContainer, StateContainer, of } from './common';
|
|
@ -19,12 +19,10 @@
|
|||
|
||||
import { UiComponent } from 'src/plugins/kibana_utils/common';
|
||||
import { ActionType, ActionContextMapping } from '../types';
|
||||
import { Presentable } from '../util/presentable';
|
||||
|
||||
export type ActionByType<T extends ActionType> = Action<ActionContextMapping[T], T>;
|
||||
|
||||
export interface Action<Context extends {} = {}, T = ActionType>
|
||||
extends Partial<Presentable<Context>> {
|
||||
export interface Action<Context = {}, T = ActionType> {
|
||||
/**
|
||||
* Determined the order when there is more than one action matched to a trigger.
|
||||
* Higher numbers are displayed first.
|
||||
|
@ -65,30 +63,12 @@ export interface Action<Context extends {} = {}, T = ActionType>
|
|||
isCompatible(context: Context): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Executes the action.
|
||||
* If this returns something truthy, this is used in addition to the `execute` method when clicked.
|
||||
*/
|
||||
execute(context: Context): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience interface used to register an action.
|
||||
*/
|
||||
export interface ActionDefinition<Context extends object = object>
|
||||
extends Partial<Presentable<Context>> {
|
||||
/**
|
||||
* ID of the action that uniquely identifies this action in the actions registry.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* ID of the factory for this action. Used to construct dynamic actions.
|
||||
*/
|
||||
readonly type?: ActionType;
|
||||
getHref?(context: Context): string | undefined;
|
||||
|
||||
/**
|
||||
* Executes the action.
|
||||
*/
|
||||
execute(context: Context): Promise<void>;
|
||||
}
|
||||
|
||||
export type ActionContext<A> = A extends ActionDefinition<infer Context> ? Context : never;
|
||||
|
|
|
@ -18,46 +18,55 @@
|
|||
*/
|
||||
|
||||
import { UiComponent } from 'src/plugins/kibana_utils/common';
|
||||
import { ActionType, ActionContextMapping } from '../types';
|
||||
|
||||
export interface ActionDefinition<T extends ActionType> {
|
||||
/**
|
||||
* Determined the order when there is more than one action matched to a trigger.
|
||||
* Higher numbers are displayed first.
|
||||
*/
|
||||
order?: number;
|
||||
|
||||
/**
|
||||
* Represents something that can be displayed to user in UI.
|
||||
* A unique identifier for this action instance.
|
||||
*/
|
||||
export interface Presentable<Context extends object = object> {
|
||||
/**
|
||||
* ID that uniquely identifies this object.
|
||||
*/
|
||||
readonly id: string;
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Determines the display order in relation to other items. Higher numbers are
|
||||
* displayed first.
|
||||
* The action type is what determines the context shape.
|
||||
*/
|
||||
readonly order: number;
|
||||
|
||||
/**
|
||||
* `UiComponent` to render when displaying this entity as a context menu item.
|
||||
* If not provided, `getDisplayName` will be used instead.
|
||||
*/
|
||||
readonly MenuItem?: UiComponent<{ context: Context }>;
|
||||
readonly type: T;
|
||||
|
||||
/**
|
||||
* Optional EUI icon type that can be displayed along with the title.
|
||||
*/
|
||||
getIconType(context: Context): string | undefined;
|
||||
getIconType?(context: ActionContextMapping[T]): string;
|
||||
|
||||
/**
|
||||
* Returns a title to be displayed to the user.
|
||||
* @param context
|
||||
*/
|
||||
getDisplayName(context: Context): string;
|
||||
getDisplayName?(context: ActionContextMapping[T]): string;
|
||||
|
||||
/**
|
||||
* This method should return a link if this item can be clicked on.
|
||||
* `UiComponent` to render when displaying this action as a context menu item.
|
||||
* If not provided, `getDisplayName` will be used instead.
|
||||
*/
|
||||
getHref?(context: Context): string | undefined;
|
||||
MenuItem?: UiComponent<{ context: ActionContextMapping[T] }>;
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to true if this item is compatible given
|
||||
* the context and should be displayed to user, otherwise resolves to false.
|
||||
* Returns a promise that resolves to true if this action is compatible given the context,
|
||||
* otherwise resolves to false.
|
||||
*/
|
||||
isCompatible(context: Context): Promise<boolean>;
|
||||
isCompatible?(context: ActionContextMapping[T]): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* If this returns something truthy, this is used in addition to the `execute` method when clicked.
|
||||
*/
|
||||
getHref?(context: ActionContextMapping[T]): string | undefined;
|
||||
|
||||
/**
|
||||
* Executes the action.
|
||||
*/
|
||||
execute(context: ActionContextMapping[T]): Promise<void>;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { uiToReactComponent } from '../../../kibana_react/public';
|
||||
import { Presentable } from '../util/presentable';
|
||||
import { ActionDefinition } from './action';
|
||||
import { ActionFactoryDefinition } from './action_factory_definition';
|
||||
import { Configurable } from '../util';
|
||||
import { SerializedAction } from './types';
|
||||
|
||||
export class ActionFactory<
|
||||
Config extends object = object,
|
||||
FactoryContext extends object = object,
|
||||
ActionContext extends object = object
|
||||
> implements Presentable<FactoryContext>, Configurable<Config, FactoryContext> {
|
||||
constructor(
|
||||
protected readonly def: ActionFactoryDefinition<Config, FactoryContext, ActionContext>
|
||||
) {}
|
||||
|
||||
public readonly id = this.def.id;
|
||||
public readonly order = this.def.order || 0;
|
||||
public readonly MenuItem? = this.def.MenuItem;
|
||||
public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined;
|
||||
|
||||
public readonly CollectConfig = this.def.CollectConfig;
|
||||
public readonly ReactCollectConfig = uiToReactComponent(this.CollectConfig);
|
||||
public readonly createConfig = this.def.createConfig;
|
||||
public readonly isConfigValid = this.def.isConfigValid;
|
||||
|
||||
public getIconType(context: FactoryContext): string | undefined {
|
||||
if (!this.def.getIconType) return undefined;
|
||||
return this.def.getIconType(context);
|
||||
}
|
||||
|
||||
public getDisplayName(context: FactoryContext): string {
|
||||
if (!this.def.getDisplayName) return '';
|
||||
return this.def.getDisplayName(context);
|
||||
}
|
||||
|
||||
public async isCompatible(context: FactoryContext): Promise<boolean> {
|
||||
if (!this.def.isCompatible) return true;
|
||||
return await this.def.isCompatible(context);
|
||||
}
|
||||
|
||||
public getHref(context: FactoryContext): string | undefined {
|
||||
if (!this.def.getHref) return undefined;
|
||||
return this.def.getHref(context);
|
||||
}
|
||||
|
||||
public create(
|
||||
serializedAction: Omit<SerializedAction<Config>, 'factoryId'>
|
||||
): ActionDefinition<ActionContext> {
|
||||
return this.def.create(serializedAction);
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ActionDefinition } from './action';
|
||||
import { Presentable, Configurable } from '../util';
|
||||
import { SerializedAction } from './types';
|
||||
|
||||
/**
|
||||
* This is a convenience interface for registering new action factories.
|
||||
*/
|
||||
export interface ActionFactoryDefinition<
|
||||
Config extends object = object,
|
||||
FactoryContext extends object = object,
|
||||
ActionContext extends object = object
|
||||
> extends Partial<Presentable<FactoryContext>>, Configurable<Config, FactoryContext> {
|
||||
/**
|
||||
* Unique ID of the action factory. This ID is used to identify this action
|
||||
* factory in the registry as well as to construct actions of this type and
|
||||
* identify this action factory when presenting it to the user in UI.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* This method should return a definition of a new action, normally used to
|
||||
* register it in `ui_actions` registry.
|
||||
*/
|
||||
create(
|
||||
serializedAction: Omit<SerializedAction<Config>, 'factoryId'>
|
||||
): ActionDefinition<ActionContext>;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ActionDefinition } from './action';
|
||||
import { ActionInternal } from './action_internal';
|
||||
|
||||
const defaultActionDef: ActionDefinition = {
|
||||
id: 'test-action',
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ActionInternal', () => {
|
||||
test('can instantiate from action definition', () => {
|
||||
const action = new ActionInternal(defaultActionDef);
|
||||
expect(action.id).toBe('test-action');
|
||||
});
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Action, ActionContext as Context, ActionDefinition } from './action';
|
||||
import { Presentable } from '../util/presentable';
|
||||
import { uiToReactComponent } from '../../../kibana_react/public';
|
||||
import { ActionType } from '../types';
|
||||
|
||||
export class ActionInternal<A extends ActionDefinition = ActionDefinition>
|
||||
implements Action<Context<A>>, Presentable<Context<A>> {
|
||||
constructor(public readonly definition: A) {}
|
||||
|
||||
public readonly id: string = this.definition.id;
|
||||
public readonly type: ActionType = this.definition.type || '';
|
||||
public readonly order: number = this.definition.order || 0;
|
||||
public readonly MenuItem? = this.definition.MenuItem;
|
||||
public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined;
|
||||
|
||||
public execute(context: Context<A>) {
|
||||
return this.definition.execute(context);
|
||||
}
|
||||
|
||||
public getIconType(context: Context<A>): string | undefined {
|
||||
if (!this.definition.getIconType) return undefined;
|
||||
return this.definition.getIconType(context);
|
||||
}
|
||||
|
||||
public getDisplayName(context: Context<A>): string {
|
||||
if (!this.definition.getDisplayName) return `Action: ${this.id}`;
|
||||
return this.definition.getDisplayName(context);
|
||||
}
|
||||
|
||||
public async isCompatible(context: Context<A>): Promise<boolean> {
|
||||
if (!this.definition.isCompatible) return true;
|
||||
return await this.definition.isCompatible(context);
|
||||
}
|
||||
|
||||
public getHref(context: Context<A>): string | undefined {
|
||||
if (!this.definition.getHref) return undefined;
|
||||
return this.definition.getHref(context);
|
||||
}
|
||||
}
|
|
@ -17,19 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ActionContextMapping } from '../types';
|
||||
import { ActionByType } from './action';
|
||||
import { ActionType } from '../types';
|
||||
import { ActionDefinition } from './action';
|
||||
import { ActionDefinition } from './action_definition';
|
||||
|
||||
interface ActionDefinitionByType<T extends ActionType>
|
||||
extends Omit<ActionDefinition<ActionContextMapping[T]>, 'id'> {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function createAction<T extends ActionType>(
|
||||
action: ActionDefinitionByType<T>
|
||||
): ActionByType<T> {
|
||||
export function createAction<T extends ActionType>(action: ActionDefinition<T>): ActionByType<T> {
|
||||
return {
|
||||
getIconType: () => undefined,
|
||||
order: 0,
|
||||
|
@ -38,5 +30,5 @@ export function createAction<T extends ActionType>(
|
|||
getDisplayName: () => '',
|
||||
getHref: () => undefined,
|
||||
...action,
|
||||
} as ActionByType<T>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,646 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { DynamicActionManager } from './dynamic_action_manager';
|
||||
import { ActionStorage, MemoryActionStorage, SerializedEvent } from './dynamic_action_storage';
|
||||
import { UiActionsService } from '../service';
|
||||
import { ActionFactoryDefinition } from './action_factory_definition';
|
||||
import { ActionRegistry } from '../types';
|
||||
import { SerializedAction } from './types';
|
||||
import { of } from '../../../kibana_utils';
|
||||
|
||||
const actionFactoryDefinition1: ActionFactoryDefinition = {
|
||||
id: 'ACTION_FACTORY_1',
|
||||
CollectConfig: {} as any,
|
||||
createConfig: () => ({}),
|
||||
isConfigValid: (() => true) as any,
|
||||
create: ({ name }) => ({
|
||||
id: '',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => name,
|
||||
}),
|
||||
};
|
||||
|
||||
const actionFactoryDefinition2: ActionFactoryDefinition = {
|
||||
id: 'ACTION_FACTORY_2',
|
||||
CollectConfig: {} as any,
|
||||
createConfig: () => ({}),
|
||||
isConfigValid: (() => true) as any,
|
||||
create: ({ name }) => ({
|
||||
id: '',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => name,
|
||||
}),
|
||||
};
|
||||
|
||||
const event1: SerializedEvent = {
|
||||
eventId: 'EVENT_ID_1',
|
||||
triggers: ['VALUE_CLICK_TRIGGER'],
|
||||
action: {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'Action 1',
|
||||
config: {},
|
||||
},
|
||||
};
|
||||
|
||||
const event2: SerializedEvent = {
|
||||
eventId: 'EVENT_ID_2',
|
||||
triggers: ['VALUE_CLICK_TRIGGER'],
|
||||
action: {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'Action 2',
|
||||
config: {},
|
||||
},
|
||||
};
|
||||
|
||||
const event3: SerializedEvent = {
|
||||
eventId: 'EVENT_ID_3',
|
||||
triggers: ['VALUE_CLICK_TRIGGER'],
|
||||
action: {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'Action 3',
|
||||
config: {},
|
||||
},
|
||||
};
|
||||
|
||||
const setup = (events: readonly SerializedEvent[] = []) => {
|
||||
const isCompatible = async () => true;
|
||||
const storage: ActionStorage = new MemoryActionStorage(events);
|
||||
const actions: ActionRegistry = new Map();
|
||||
const uiActions = new UiActionsService({
|
||||
actions,
|
||||
});
|
||||
const manager = new DynamicActionManager({
|
||||
isCompatible,
|
||||
storage,
|
||||
uiActions,
|
||||
});
|
||||
|
||||
uiActions.registerTrigger({
|
||||
id: 'VALUE_CLICK_TRIGGER',
|
||||
});
|
||||
|
||||
return {
|
||||
isCompatible,
|
||||
actions,
|
||||
storage,
|
||||
uiActions,
|
||||
manager,
|
||||
};
|
||||
};
|
||||
|
||||
describe('DynamicActionManager', () => {
|
||||
test('can instantiate', () => {
|
||||
const { manager } = setup([event1]);
|
||||
expect(manager).toBeInstanceOf(DynamicActionManager);
|
||||
});
|
||||
|
||||
describe('.start()', () => {
|
||||
test('instantiates stored events', async () => {
|
||||
const { manager, actions, uiActions } = setup([event1]);
|
||||
const create1 = jest.fn();
|
||||
const create2 = jest.fn();
|
||||
|
||||
uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 });
|
||||
uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 });
|
||||
|
||||
expect(create1).toHaveBeenCalledTimes(0);
|
||||
expect(create2).toHaveBeenCalledTimes(0);
|
||||
expect(actions.size).toBe(0);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(create1).toHaveBeenCalledTimes(1);
|
||||
expect(create2).toHaveBeenCalledTimes(0);
|
||||
expect(actions.size).toBe(1);
|
||||
});
|
||||
|
||||
test('does nothing when no events stored', async () => {
|
||||
const { manager, actions, uiActions } = setup();
|
||||
const create1 = jest.fn();
|
||||
const create2 = jest.fn();
|
||||
|
||||
uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 });
|
||||
uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 });
|
||||
|
||||
expect(create1).toHaveBeenCalledTimes(0);
|
||||
expect(create2).toHaveBeenCalledTimes(0);
|
||||
expect(actions.size).toBe(0);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(create1).toHaveBeenCalledTimes(0);
|
||||
expect(create2).toHaveBeenCalledTimes(0);
|
||||
expect(actions.size).toBe(0);
|
||||
});
|
||||
|
||||
test('UI state is empty before manager starts', async () => {
|
||||
const { manager } = setup([event1]);
|
||||
|
||||
expect(manager.state.get()).toMatchObject({
|
||||
events: [],
|
||||
isFetchingEvents: false,
|
||||
fetchCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('loads events into UI state', async () => {
|
||||
const { manager, uiActions } = setup([event1, event2, event3]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(manager.state.get()).toMatchObject({
|
||||
events: [event1, event2, event3],
|
||||
isFetchingEvents: false,
|
||||
fetchCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('sets isFetchingEvents to true while fetching events', async () => {
|
||||
const { manager, uiActions } = setup([event1, event2, event3]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
|
||||
const promise = manager.start().catch(() => {});
|
||||
|
||||
expect(manager.state.get().isFetchingEvents).toBe(true);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(manager.state.get().isFetchingEvents).toBe(false);
|
||||
});
|
||||
|
||||
test('throws if storage threw', async () => {
|
||||
const { manager, storage } = setup([event1]);
|
||||
|
||||
storage.list = async () => {
|
||||
throw new Error('baz');
|
||||
};
|
||||
|
||||
const [, error] = await of(manager.start());
|
||||
|
||||
expect(error).toEqual(new Error('baz'));
|
||||
});
|
||||
|
||||
test('sets UI state error if error happened during initial fetch', async () => {
|
||||
const { manager, storage } = setup([event1]);
|
||||
|
||||
storage.list = async () => {
|
||||
throw new Error('baz');
|
||||
};
|
||||
|
||||
await of(manager.start());
|
||||
|
||||
expect(manager.state.get().fetchError!.message).toBe('baz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.stop()', () => {
|
||||
test('removes events from UI actions registry', async () => {
|
||||
const { manager, actions, uiActions } = setup([event1, event2]);
|
||||
const create1 = jest.fn();
|
||||
const create2 = jest.fn();
|
||||
|
||||
uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 });
|
||||
uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 });
|
||||
|
||||
expect(actions.size).toBe(0);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(actions.size).toBe(2);
|
||||
|
||||
await manager.stop();
|
||||
|
||||
expect(actions.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.createEvent()', () => {
|
||||
describe('when storage succeeds', () => {
|
||||
test('stores new event in storage', async () => {
|
||||
const { manager, storage, uiActions } = setup([]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
expect(await storage.count()).toBe(0);
|
||||
|
||||
await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']);
|
||||
|
||||
expect(await storage.count()).toBe(1);
|
||||
|
||||
const [event] = await storage.list();
|
||||
|
||||
expect(event).toMatchObject({
|
||||
eventId: expect.any(String),
|
||||
triggers: ['VALUE_CLICK_TRIGGER'],
|
||||
action: {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('adds event to UI state', async () => {
|
||||
const { manager, uiActions } = setup([]);
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(manager.state.get().events.length).toBe(0);
|
||||
|
||||
await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']);
|
||||
|
||||
expect(manager.state.get().events.length).toBe(1);
|
||||
});
|
||||
|
||||
test('optimistically adds event to UI state', async () => {
|
||||
const { manager, uiActions } = setup([]);
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(manager.state.get().events.length).toBe(0);
|
||||
|
||||
const promise = manager.createEvent(action, ['VALUE_CLICK_TRIGGER']).catch(e => e);
|
||||
|
||||
expect(manager.state.get().events.length).toBe(1);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(manager.state.get().events.length).toBe(1);
|
||||
});
|
||||
|
||||
test('instantiates event in actions service', async () => {
|
||||
const { manager, uiActions, actions } = setup([]);
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(actions.size).toBe(0);
|
||||
|
||||
await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']);
|
||||
|
||||
expect(actions.size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when storage fails', () => {
|
||||
test('throws an error', async () => {
|
||||
const { manager, storage, uiActions } = setup([]);
|
||||
|
||||
storage.create = async () => {
|
||||
throw new Error('foo');
|
||||
};
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
const [, error] = await of(manager.createEvent(action, ['VALUE_CLICK_TRIGGER']));
|
||||
|
||||
expect(error).toEqual(new Error('foo'));
|
||||
});
|
||||
|
||||
test('does not add even to UI state', async () => {
|
||||
const { manager, storage, uiActions } = setup([]);
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
storage.create = async () => {
|
||||
throw new Error('foo');
|
||||
};
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
await of(manager.createEvent(action, ['VALUE_CLICK_TRIGGER']));
|
||||
|
||||
expect(manager.state.get().events.length).toBe(0);
|
||||
});
|
||||
|
||||
test('optimistically adds event to UI state and then removes it', async () => {
|
||||
const { manager, storage, uiActions } = setup([]);
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
storage.create = async () => {
|
||||
throw new Error('foo');
|
||||
};
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(manager.state.get().events.length).toBe(0);
|
||||
|
||||
const promise = manager.createEvent(action, ['VALUE_CLICK_TRIGGER']).catch(e => e);
|
||||
|
||||
expect(manager.state.get().events.length).toBe(1);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(manager.state.get().events.length).toBe(0);
|
||||
});
|
||||
|
||||
test('does not instantiate event in actions service', async () => {
|
||||
const { manager, storage, uiActions, actions } = setup([]);
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition1.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
storage.create = async () => {
|
||||
throw new Error('foo');
|
||||
};
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(actions.size).toBe(0);
|
||||
|
||||
await of(manager.createEvent(action, ['VALUE_CLICK_TRIGGER']));
|
||||
|
||||
expect(actions.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.updateEvent()', () => {
|
||||
describe('when storage succeeds', () => {
|
||||
test('un-registers old event from ui actions service and registers the new one', async () => {
|
||||
const { manager, actions, uiActions } = setup([event3]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
await manager.start();
|
||||
|
||||
expect(actions.size).toBe(1);
|
||||
|
||||
const registeredAction1 = actions.values().next().value;
|
||||
|
||||
expect(registeredAction1.getDisplayName()).toBe('Action 3');
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
await manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']);
|
||||
|
||||
expect(actions.size).toBe(1);
|
||||
|
||||
const registeredAction2 = actions.values().next().value;
|
||||
|
||||
expect(registeredAction2.getDisplayName()).toBe('foo');
|
||||
});
|
||||
|
||||
test('updates event in storage', async () => {
|
||||
const { manager, storage, uiActions } = setup([event3]);
|
||||
const storageUpdateSpy = jest.spyOn(storage, 'update');
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
expect(storageUpdateSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']);
|
||||
|
||||
expect(storageUpdateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(storageUpdateSpy.mock.calls[0][0]).toMatchObject({
|
||||
eventId: expect.any(String),
|
||||
triggers: ['VALUE_CLICK_TRIGGER'],
|
||||
action: {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('updates event in UI state', async () => {
|
||||
const { manager, uiActions } = setup([event3]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
expect(manager.state.get().events[0].action.name).toBe('Action 3');
|
||||
|
||||
await manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']);
|
||||
|
||||
expect(manager.state.get().events[0].action.name).toBe('foo');
|
||||
});
|
||||
|
||||
test('optimistically updates event in UI state', async () => {
|
||||
const { manager, uiActions } = setup([event3]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
expect(manager.state.get().events[0].action.name).toBe('Action 3');
|
||||
|
||||
const promise = manager
|
||||
.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER'])
|
||||
.catch(e => e);
|
||||
|
||||
expect(manager.state.get().events[0].action.name).toBe('foo');
|
||||
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when storage fails', () => {
|
||||
test('throws error', async () => {
|
||||
const { manager, storage, uiActions } = setup([event3]);
|
||||
|
||||
storage.update = () => {
|
||||
throw new Error('bar');
|
||||
};
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
const [, error] = await of(
|
||||
manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER'])
|
||||
);
|
||||
|
||||
expect(error).toEqual(new Error('bar'));
|
||||
});
|
||||
|
||||
test('keeps the old action in actions registry', async () => {
|
||||
const { manager, storage, actions, uiActions } = setup([event3]);
|
||||
|
||||
storage.update = () => {
|
||||
throw new Error('bar');
|
||||
};
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
await manager.start();
|
||||
|
||||
expect(actions.size).toBe(1);
|
||||
|
||||
const registeredAction1 = actions.values().next().value;
|
||||
|
||||
expect(registeredAction1.getDisplayName()).toBe('Action 3');
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
await of(manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']));
|
||||
|
||||
expect(actions.size).toBe(1);
|
||||
|
||||
const registeredAction2 = actions.values().next().value;
|
||||
|
||||
expect(registeredAction2.getDisplayName()).toBe('Action 3');
|
||||
});
|
||||
|
||||
test('keeps old event in UI state', async () => {
|
||||
const { manager, storage, uiActions } = setup([event3]);
|
||||
|
||||
storage.update = () => {
|
||||
throw new Error('bar');
|
||||
};
|
||||
uiActions.registerActionFactory(actionFactoryDefinition2);
|
||||
await manager.start();
|
||||
|
||||
const action: SerializedAction<unknown> = {
|
||||
factoryId: actionFactoryDefinition2.id,
|
||||
name: 'foo',
|
||||
config: {},
|
||||
};
|
||||
|
||||
expect(manager.state.get().events[0].action.name).toBe('Action 3');
|
||||
|
||||
await of(manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']));
|
||||
|
||||
expect(manager.state.get().events[0].action.name).toBe('Action 3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.deleteEvents()', () => {
|
||||
describe('when storage succeeds', () => {
|
||||
test('removes all actions from uiActions service', async () => {
|
||||
const { manager, actions, uiActions } = setup([event2, event1]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(actions.size).toBe(2);
|
||||
|
||||
await manager.deleteEvents([event1.eventId, event2.eventId]);
|
||||
|
||||
expect(actions.size).toBe(0);
|
||||
});
|
||||
|
||||
test('removes all events from storage', async () => {
|
||||
const { manager, uiActions, storage } = setup([event2, event1]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(await storage.list()).toEqual([event2, event1]);
|
||||
|
||||
await manager.deleteEvents([event1.eventId, event2.eventId]);
|
||||
|
||||
expect(await storage.list()).toEqual([]);
|
||||
});
|
||||
|
||||
test('removes all events from UI state', async () => {
|
||||
const { manager, uiActions } = setup([event2, event1]);
|
||||
|
||||
uiActions.registerActionFactory(actionFactoryDefinition1);
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(manager.state.get().events).toEqual([event2, event1]);
|
||||
|
||||
await manager.deleteEvents([event1.eventId, event2.eventId]);
|
||||
|
||||
expect(manager.state.get().events).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,284 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ActionStorage, SerializedEvent } from './dynamic_action_storage';
|
||||
import { UiActionsService } from '../service';
|
||||
import { SerializedAction } from './types';
|
||||
import { TriggerContextMapping } from '../types';
|
||||
import { ActionDefinition } from './action';
|
||||
import { defaultState, transitions, selectors, State } from './dynamic_action_manager_state';
|
||||
import { StateContainer, createStateContainer } from '../../../kibana_utils';
|
||||
|
||||
const compareEvents = (
|
||||
a: ReadonlyArray<{ eventId: string }>,
|
||||
b: ReadonlyArray<{ eventId: string }>
|
||||
) => {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) if (a[i].eventId !== b[i].eventId) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export type DynamicActionManagerState = State;
|
||||
|
||||
export interface DynamicActionManagerParams {
|
||||
storage: ActionStorage;
|
||||
uiActions: Pick<
|
||||
UiActionsService,
|
||||
'registerAction' | 'attachAction' | 'unregisterAction' | 'detachAction' | 'getActionFactory'
|
||||
>;
|
||||
isCompatible: <C = unknown>(context: C) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export class DynamicActionManager {
|
||||
static idPrefixCounter = 0;
|
||||
|
||||
private readonly idPrefix = `D_ACTION_${DynamicActionManager.idPrefixCounter++}_`;
|
||||
private stopped: boolean = false;
|
||||
private reloadSubscription?: Subscription;
|
||||
|
||||
/**
|
||||
* UI State of the dynamic action manager.
|
||||
*/
|
||||
protected readonly ui = createStateContainer(defaultState, transitions, selectors);
|
||||
|
||||
constructor(protected readonly params: DynamicActionManagerParams) {}
|
||||
|
||||
protected getEvent(eventId: string): SerializedEvent {
|
||||
const oldEvent = this.ui.selectors.getEvent(eventId);
|
||||
if (!oldEvent) throw new Error(`Could not find event [eventId = ${eventId}].`);
|
||||
return oldEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* We prefix action IDs with a unique `.idPrefix`, so we can render the
|
||||
* same dashboard twice on the screen.
|
||||
*/
|
||||
protected generateActionId(eventId: string): string {
|
||||
return this.idPrefix + eventId;
|
||||
}
|
||||
|
||||
protected reviveAction(event: SerializedEvent) {
|
||||
const { eventId, triggers, action } = event;
|
||||
const { uiActions, isCompatible } = this.params;
|
||||
|
||||
const actionId = this.generateActionId(eventId);
|
||||
const factory = uiActions.getActionFactory(event.action.factoryId);
|
||||
const actionDefinition: ActionDefinition = {
|
||||
...factory.create(action as SerializedAction<object>),
|
||||
id: actionId,
|
||||
isCompatible,
|
||||
};
|
||||
|
||||
uiActions.registerAction(actionDefinition);
|
||||
for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId);
|
||||
}
|
||||
|
||||
protected killAction({ eventId, triggers }: SerializedEvent) {
|
||||
const { uiActions } = this.params;
|
||||
const actionId = this.generateActionId(eventId);
|
||||
|
||||
for (const trigger of triggers) uiActions.detachAction(trigger as any, actionId);
|
||||
uiActions.unregisterAction(actionId);
|
||||
}
|
||||
|
||||
private syncId = 0;
|
||||
|
||||
/**
|
||||
* This function is called every time stored events might have changed not by
|
||||
* us. For example, when in edit mode on dashboard user presses "back" button
|
||||
* in the browser, then contents of storage changes.
|
||||
*/
|
||||
private onSync = () => {
|
||||
if (this.stopped) return;
|
||||
|
||||
(async () => {
|
||||
const syncId = ++this.syncId;
|
||||
const events = await this.params.storage.list();
|
||||
|
||||
if (this.stopped) return;
|
||||
if (syncId !== this.syncId) return;
|
||||
if (compareEvents(events, this.ui.get().events)) return;
|
||||
|
||||
for (const event of this.ui.get().events) this.killAction(event);
|
||||
for (const event of events) this.reviveAction(event);
|
||||
this.ui.transitions.finishFetching(events);
|
||||
})().catch(error => {
|
||||
/* eslint-disable */
|
||||
console.log('Dynamic action manager storage reload failed.');
|
||||
console.error(error);
|
||||
/* eslint-enable */
|
||||
});
|
||||
};
|
||||
|
||||
// Public API: ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Read-only state container of dynamic action manager. Use it to perform all
|
||||
* *read* operations.
|
||||
*/
|
||||
public readonly state: StateContainer<State> = this.ui;
|
||||
|
||||
/**
|
||||
* 1. Loads all events from @type {DynamicActionStorage} storage.
|
||||
* 2. Creates actions for each event in `ui_actions` registry.
|
||||
* 3. Adds events to UI state.
|
||||
* 4. Does nothing if dynamic action manager was stopped or if event fetching
|
||||
* is already taking place.
|
||||
*/
|
||||
public async start() {
|
||||
if (this.stopped) return;
|
||||
if (this.ui.get().isFetchingEvents) return;
|
||||
|
||||
this.ui.transitions.startFetching();
|
||||
try {
|
||||
const events = await this.params.storage.list();
|
||||
for (const event of events) this.reviveAction(event);
|
||||
this.ui.transitions.finishFetching(events);
|
||||
} catch (error) {
|
||||
this.ui.transitions.failFetching(error instanceof Error ? error : { message: String(error) });
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (this.params.storage.reload$) {
|
||||
this.reloadSubscription = this.params.storage.reload$.subscribe(this.onSync);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Removes all events from `ui_actions` registry.
|
||||
* 2. Puts dynamic action manager is stopped state.
|
||||
*/
|
||||
public async stop() {
|
||||
this.stopped = true;
|
||||
const events = await this.params.storage.list();
|
||||
|
||||
for (const event of events) {
|
||||
this.killAction(event);
|
||||
}
|
||||
|
||||
if (this.reloadSubscription) {
|
||||
this.reloadSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new event.
|
||||
*
|
||||
* 1. Stores event in @type {DynamicActionStorage} storage.
|
||||
* 2. Optimistically adds it to UI state, and rolls back on failure.
|
||||
* 3. Adds action to `ui_actions` registry.
|
||||
*
|
||||
* @param action Dynamic action for which to create an event.
|
||||
* @param triggers List of triggers to which action should react.
|
||||
*/
|
||||
public async createEvent(
|
||||
action: SerializedAction<unknown>,
|
||||
triggers: Array<keyof TriggerContextMapping>
|
||||
) {
|
||||
const event: SerializedEvent = {
|
||||
eventId: uuidv4(),
|
||||
triggers,
|
||||
action,
|
||||
};
|
||||
|
||||
this.ui.transitions.addEvent(event);
|
||||
try {
|
||||
await this.params.storage.create(event);
|
||||
this.reviveAction(event);
|
||||
} catch (error) {
|
||||
this.ui.transitions.removeEvent(event.eventId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing event. Fails if event with given `eventId` does not
|
||||
* exit.
|
||||
*
|
||||
* 1. Updates the event in @type {DynamicActionStorage} storage.
|
||||
* 2. Optimistically replaces the old event by the new one in UI state, and
|
||||
* rolls back on failure.
|
||||
* 3. Replaces action in `ui_actions` registry with the new event.
|
||||
*
|
||||
*
|
||||
* @param eventId ID of the event to replace.
|
||||
* @param action New action for which to create the event.
|
||||
* @param triggers List of triggers to which action should react.
|
||||
*/
|
||||
public async updateEvent(
|
||||
eventId: string,
|
||||
action: SerializedAction<unknown>,
|
||||
triggers: Array<keyof TriggerContextMapping>
|
||||
) {
|
||||
const event: SerializedEvent = {
|
||||
eventId,
|
||||
triggers,
|
||||
action,
|
||||
};
|
||||
const oldEvent = this.getEvent(eventId);
|
||||
this.killAction(oldEvent);
|
||||
|
||||
this.reviveAction(event);
|
||||
this.ui.transitions.replaceEvent(event);
|
||||
|
||||
try {
|
||||
await this.params.storage.update(event);
|
||||
} catch (error) {
|
||||
this.killAction(event);
|
||||
this.reviveAction(oldEvent);
|
||||
this.ui.transitions.replaceEvent(oldEvent);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes existing event. Throws if event does not exist.
|
||||
*
|
||||
* 1. Removes the event from @type {DynamicActionStorage} storage.
|
||||
* 2. Optimistically removes event from UI state, and puts it back on failure.
|
||||
* 3. Removes associated action from `ui_actions` registry.
|
||||
*
|
||||
* @param eventId ID of the event to remove.
|
||||
*/
|
||||
public async deleteEvent(eventId: string) {
|
||||
const event = this.getEvent(eventId);
|
||||
|
||||
this.killAction(event);
|
||||
this.ui.transitions.removeEvent(eventId);
|
||||
|
||||
try {
|
||||
await this.params.storage.remove(eventId);
|
||||
} catch (error) {
|
||||
this.reviveAction(event);
|
||||
this.ui.transitions.addEvent(event);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple events at once.
|
||||
*
|
||||
* @param eventIds List of event IDs.
|
||||
*/
|
||||
public async deleteEvents(eventIds: string[]) {
|
||||
await Promise.all(eventIds.map(this.deleteEvent.bind(this)));
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SerializedEvent } from './dynamic_action_storage';
|
||||
|
||||
/**
|
||||
* This interface represents the state of @type {DynamicActionManager} at any
|
||||
* point in time.
|
||||
*/
|
||||
export interface State {
|
||||
/**
|
||||
* Whether dynamic action manager is currently in process of fetching events
|
||||
* from storage.
|
||||
*/
|
||||
readonly isFetchingEvents: boolean;
|
||||
|
||||
/**
|
||||
* Number of times event fetching has been completed.
|
||||
*/
|
||||
readonly fetchCount: number;
|
||||
|
||||
/**
|
||||
* Error received last time when fetching events.
|
||||
*/
|
||||
readonly fetchError?: {
|
||||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* List of all fetched events.
|
||||
*/
|
||||
readonly events: readonly SerializedEvent[];
|
||||
}
|
||||
|
||||
export interface Transitions {
|
||||
startFetching: (state: State) => () => State;
|
||||
finishFetching: (state: State) => (events: SerializedEvent[]) => State;
|
||||
failFetching: (state: State) => (error: { message: string }) => State;
|
||||
addEvent: (state: State) => (event: SerializedEvent) => State;
|
||||
removeEvent: (state: State) => (eventId: string) => State;
|
||||
replaceEvent: (state: State) => (event: SerializedEvent) => State;
|
||||
}
|
||||
|
||||
export interface Selectors {
|
||||
getEvent: (state: State) => (eventId: string) => SerializedEvent | null;
|
||||
}
|
||||
|
||||
export const defaultState: State = {
|
||||
isFetchingEvents: false,
|
||||
fetchCount: 0,
|
||||
events: [],
|
||||
};
|
||||
|
||||
export const transitions: Transitions = {
|
||||
startFetching: state => () => ({ ...state, isFetchingEvents: true }),
|
||||
|
||||
finishFetching: state => events => ({
|
||||
...state,
|
||||
isFetchingEvents: false,
|
||||
fetchCount: state.fetchCount + 1,
|
||||
fetchError: undefined,
|
||||
events,
|
||||
}),
|
||||
|
||||
failFetching: state => ({ message }) => ({
|
||||
...state,
|
||||
isFetchingEvents: false,
|
||||
fetchCount: state.fetchCount + 1,
|
||||
fetchError: { message },
|
||||
}),
|
||||
|
||||
addEvent: state => (event: SerializedEvent) => ({
|
||||
...state,
|
||||
events: [...state.events, event],
|
||||
}),
|
||||
|
||||
removeEvent: state => (eventId: string) => ({
|
||||
...state,
|
||||
events: state.events ? state.events.filter(event => event.eventId !== eventId) : state.events,
|
||||
}),
|
||||
|
||||
replaceEvent: state => event => {
|
||||
const index = state.events.findIndex(({ eventId }) => eventId === event.eventId);
|
||||
if (index === -1) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
events: [...state.events.slice(0, index), event, ...state.events.slice(index + 1)],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const selectors: Selectors = {
|
||||
getEvent: state => eventId => state.events.find(event => event.eventId === eventId) || null,
|
||||
};
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { SerializedAction } from './types';
|
||||
|
||||
/**
|
||||
* Serialized representation of event-action pair, used to persist in storage.
|
||||
*/
|
||||
export interface SerializedEvent {
|
||||
eventId: string;
|
||||
triggers: string[];
|
||||
action: SerializedAction<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This CRUD interface needs to be implemented by dynamic action users if they
|
||||
* want to persist the dynamic actions. It has a default implementation in
|
||||
* Embeddables, however one can use the dynamic actions without Embeddables,
|
||||
* in that case they have to implement this interface.
|
||||
*/
|
||||
export interface ActionStorage {
|
||||
create(event: SerializedEvent): Promise<void>;
|
||||
update(event: SerializedEvent): Promise<void>;
|
||||
remove(eventId: string): Promise<void>;
|
||||
read(eventId: string): Promise<SerializedEvent>;
|
||||
count(): Promise<number>;
|
||||
list(): Promise<SerializedEvent[]>;
|
||||
|
||||
/**
|
||||
* Triggered every time events changed in storage and should be re-loaded.
|
||||
*/
|
||||
readonly reload$?: Observable<void>;
|
||||
}
|
||||
|
||||
export abstract class AbstractActionStorage implements ActionStorage {
|
||||
public readonly reload$: Observable<void> & Pick<Subject<void>, 'next'> = new Subject<void>();
|
||||
|
||||
public async count(): Promise<number> {
|
||||
return (await this.list()).length;
|
||||
}
|
||||
|
||||
public async read(eventId: string): Promise<SerializedEvent> {
|
||||
const events = await this.list();
|
||||
const event = events.find(ev => ev.eventId === eventId);
|
||||
if (!event) throw new Error(`Event [eventId = ${eventId}] not found.`);
|
||||
return event;
|
||||
}
|
||||
|
||||
abstract create(event: SerializedEvent): Promise<void>;
|
||||
abstract update(event: SerializedEvent): Promise<void>;
|
||||
abstract remove(eventId: string): Promise<void>;
|
||||
abstract list(): Promise<SerializedEvent[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an in-memory implementation of ActionStorage. It is used in testing,
|
||||
* but can also be used production code to store events in memory.
|
||||
*/
|
||||
export class MemoryActionStorage extends AbstractActionStorage {
|
||||
constructor(public events: readonly SerializedEvent[] = []) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async list() {
|
||||
return this.events.map(event => ({ ...event }));
|
||||
}
|
||||
|
||||
public async create(event: SerializedEvent) {
|
||||
this.events = [...this.events, { ...event }];
|
||||
}
|
||||
|
||||
public async update(event: SerializedEvent) {
|
||||
const index = this.events.findIndex(({ eventId }) => eventId === event.eventId);
|
||||
if (index < 0) throw new Error(`Event [eventId = ${event.eventId}] not found`);
|
||||
this.events = [...this.events.slice(0, index), { ...event }, ...this.events.slice(index + 1)];
|
||||
}
|
||||
|
||||
public async remove(eventId: string) {
|
||||
const index = this.events.findIndex(ev => eventId === ev.eventId);
|
||||
if (index < 0) throw new Error(`Event [eventId = ${eventId}] not found`);
|
||||
this.events = [...this.events.slice(0, index), ...this.events.slice(index + 1)];
|
||||
}
|
||||
}
|
|
@ -18,11 +18,5 @@
|
|||
*/
|
||||
|
||||
export * from './action';
|
||||
export * from './action_internal';
|
||||
export * from './action_factory_definition';
|
||||
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';
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export interface SerializedAction<Config> {
|
||||
readonly factoryId: string;
|
||||
readonly name: string;
|
||||
readonly config: Config;
|
||||
}
|
|
@ -24,25 +24,19 @@ import { i18n } from '@kbn/i18n';
|
|||
import { uiToReactComponent } from '../../../kibana_react/public';
|
||||
import { Action } from '../actions';
|
||||
|
||||
export const defaultTitle = i18n.translate('uiActions.actionPanel.title', {
|
||||
defaultMessage: 'Options',
|
||||
});
|
||||
|
||||
/**
|
||||
* Transforms an array of Actions to the shape EuiContextMenuPanel expects.
|
||||
*/
|
||||
export async function buildContextMenuForActions<Context extends object>({
|
||||
export async function buildContextMenuForActions<A>({
|
||||
actions,
|
||||
actionContext,
|
||||
title = defaultTitle,
|
||||
closeMenu,
|
||||
}: {
|
||||
actions: Array<Action<Context>>;
|
||||
actionContext: Context;
|
||||
title?: string;
|
||||
actions: Array<Action<A>>;
|
||||
actionContext: A;
|
||||
closeMenu: () => void;
|
||||
}): Promise<EuiContextMenuPanelDescriptor> {
|
||||
const menuItems = await buildEuiContextMenuPanelItems<Context>({
|
||||
const menuItems = await buildEuiContextMenuPanelItems<A>({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
|
@ -50,7 +44,9 @@ export async function buildContextMenuForActions<Context extends object>({
|
|||
|
||||
return {
|
||||
id: 'mainMenu',
|
||||
title,
|
||||
title: i18n.translate('uiActions.actionPanel.title', {
|
||||
defaultMessage: 'Options',
|
||||
}),
|
||||
items: menuItems,
|
||||
};
|
||||
}
|
||||
|
@ -58,41 +54,49 @@ export async function buildContextMenuForActions<Context extends object>({
|
|||
/**
|
||||
* Transform an array of Actions into the shape needed to build an EUIContextMenu
|
||||
*/
|
||||
async function buildEuiContextMenuPanelItems<Context extends object>({
|
||||
async function buildEuiContextMenuPanelItems<A>({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
}: {
|
||||
actions: Array<Action<Context>>;
|
||||
actionContext: Context;
|
||||
actions: Array<Action<A>>;
|
||||
actionContext: A;
|
||||
closeMenu: () => void;
|
||||
}) {
|
||||
const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length);
|
||||
const promises = actions.map(async (action, index) => {
|
||||
const items: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
const promises = actions.map(async action => {
|
||||
const isCompatible = await action.isCompatible(actionContext);
|
||||
if (!isCompatible) {
|
||||
return;
|
||||
}
|
||||
|
||||
items[index] = convertPanelActionToContextMenuItem({
|
||||
items.push(
|
||||
convertPanelActionToContextMenuItem({
|
||||
action,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return items.filter(Boolean);
|
||||
return items;
|
||||
}
|
||||
|
||||
function convertPanelActionToContextMenuItem<Context extends object>({
|
||||
/**
|
||||
*
|
||||
* @param {ContextMenuAction} action
|
||||
* @param {Embeddable} embeddable
|
||||
* @return {EuiContextMenuPanelItemDescriptor}
|
||||
*/
|
||||
function convertPanelActionToContextMenuItem<A>({
|
||||
action,
|
||||
actionContext,
|
||||
closeMenu,
|
||||
}: {
|
||||
action: Action<Context>;
|
||||
actionContext: Context;
|
||||
action: Action<A>;
|
||||
actionContext: A;
|
||||
closeMenu: () => void;
|
||||
}): EuiContextMenuPanelItemDescriptor {
|
||||
const menuPanelItem: EuiContextMenuPanelItemDescriptor = {
|
||||
|
@ -111,12 +115,9 @@ function convertPanelActionToContextMenuItem<Context extends object>({
|
|||
closeMenu();
|
||||
};
|
||||
|
||||
if (action.getHref) {
|
||||
const href = action.getHref(actionContext);
|
||||
if (href) {
|
||||
if (action.getHref && action.getHref(actionContext)) {
|
||||
menuPanelItem.href = action.getHref(actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
return menuPanelItem;
|
||||
}
|
||||
|
|
|
@ -26,26 +26,8 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
|
||||
export { UiActionsSetup, UiActionsStart } from './plugin';
|
||||
export { UiActionsServiceParams, UiActionsService } from './service';
|
||||
export {
|
||||
Action,
|
||||
ActionDefinition as UiActionsActionDefinition,
|
||||
ActionFactoryDefinition as UiActionsActionFactoryDefinition,
|
||||
ActionInternal as UiActionsActionInternal,
|
||||
ActionStorage as UiActionsActionStorage,
|
||||
AbstractActionStorage as UiActionsAbstractActionStorage,
|
||||
createAction,
|
||||
DynamicActionManager,
|
||||
DynamicActionManagerState,
|
||||
IncompatibleActionError,
|
||||
SerializedAction as UiActionsSerializedAction,
|
||||
SerializedEvent as UiActionsSerializedEvent,
|
||||
} from './actions';
|
||||
export { Action, createAction, IncompatibleActionError } from './actions';
|
||||
export { buildContextMenuForActions } from './context_menu';
|
||||
export {
|
||||
Presentable as UiActionsPresentable,
|
||||
Configurable as UiActionsConfigurable,
|
||||
CollectConfigProps as UiActionsCollectConfigProps,
|
||||
} from './util';
|
||||
export {
|
||||
Trigger,
|
||||
TriggerContext,
|
||||
|
@ -57,4 +39,4 @@ export {
|
|||
applyFilterTrigger,
|
||||
} from './triggers';
|
||||
export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types';
|
||||
export { ActionByType, DynamicActionManager as UiActionsDynamicActionManager } from './actions';
|
||||
export { ActionByType } from './actions';
|
||||
|
|
|
@ -28,13 +28,10 @@ export type Start = jest.Mocked<UiActionsStart>;
|
|||
|
||||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {
|
||||
addTriggerAction: jest.fn(),
|
||||
attachAction: jest.fn(),
|
||||
detachAction: jest.fn(),
|
||||
registerAction: jest.fn(),
|
||||
registerActionFactory: jest.fn(),
|
||||
registerTrigger: jest.fn(),
|
||||
unregisterAction: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
@ -42,21 +39,16 @@ const createSetupContract = (): Setup => {
|
|||
const createStartContract = (): Start => {
|
||||
const startContract: Start = {
|
||||
attachAction: jest.fn(),
|
||||
unregisterAction: jest.fn(),
|
||||
addTriggerAction: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
registerAction: jest.fn(),
|
||||
registerTrigger: jest.fn(),
|
||||
getAction: 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(),
|
||||
registerAction: jest.fn(),
|
||||
registerActionFactory: jest.fn(),
|
||||
registerTrigger: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
fork: jest.fn(),
|
||||
};
|
||||
|
||||
return startContract;
|
||||
|
|
|
@ -23,13 +23,7 @@ import { selectRangeTrigger, valueClickTrigger, applyFilterTrigger } from './tri
|
|||
|
||||
export type UiActionsSetup = Pick<
|
||||
UiActionsService,
|
||||
| 'addTriggerAction'
|
||||
| 'attachAction'
|
||||
| 'detachAction'
|
||||
| 'registerAction'
|
||||
| 'registerActionFactory'
|
||||
| 'registerTrigger'
|
||||
| 'unregisterAction'
|
||||
'attachAction' | 'detachAction' | 'registerAction' | 'registerTrigger'
|
||||
>;
|
||||
|
||||
export type UiActionsStart = PublicMethodsOf<UiActionsService>;
|
||||
|
|
|
@ -18,13 +18,7 @@
|
|||
*/
|
||||
|
||||
import { UiActionsService } from './ui_actions_service';
|
||||
import {
|
||||
Action,
|
||||
ActionInternal,
|
||||
createAction,
|
||||
ActionFactoryDefinition,
|
||||
ActionFactory,
|
||||
} from '../actions';
|
||||
import { Action, createAction } from '../actions';
|
||||
import { createHelloWorldAction } from '../tests/test_samples';
|
||||
import { ActionRegistry, TriggerRegistry, TriggerId, ActionType } from '../types';
|
||||
import { Trigger } from '../triggers';
|
||||
|
@ -108,21 +102,6 @@ describe('UiActionsService', () => {
|
|||
type: 'test' as ActionType,
|
||||
});
|
||||
});
|
||||
|
||||
test('return action instance', () => {
|
||||
const service = new UiActionsService();
|
||||
const action = service.registerAction({
|
||||
id: 'test',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test',
|
||||
getIconType: () => '',
|
||||
isCompatible: async () => true,
|
||||
type: 'test' as ActionType,
|
||||
});
|
||||
|
||||
expect(action).toBeInstanceOf(ActionInternal);
|
||||
expect(action.id).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getTriggerActions()', () => {
|
||||
|
@ -160,14 +139,13 @@ describe('UiActionsService', () => {
|
|||
|
||||
expect(list0).toHaveLength(0);
|
||||
|
||||
service.addTriggerAction(FOO_TRIGGER, action1);
|
||||
service.attachAction(FOO_TRIGGER, action1);
|
||||
const list1 = service.getTriggerActions(FOO_TRIGGER);
|
||||
|
||||
expect(list1).toHaveLength(1);
|
||||
expect(list1[0]).toBeInstanceOf(ActionInternal);
|
||||
expect(list1[0].id).toBe(action1.id);
|
||||
expect(list1).toEqual([action1]);
|
||||
|
||||
service.addTriggerAction(FOO_TRIGGER, action2);
|
||||
service.attachAction(FOO_TRIGGER, action2);
|
||||
const list2 = service.getTriggerActions(FOO_TRIGGER);
|
||||
|
||||
expect(list2).toHaveLength(2);
|
||||
|
@ -186,7 +164,7 @@ describe('UiActionsService', () => {
|
|||
service.registerAction(helloWorldAction);
|
||||
|
||||
expect(actions.size - length).toBe(1);
|
||||
expect(actions.get(helloWorldAction.id)!.id).toBe(helloWorldAction.id);
|
||||
expect(actions.get(helloWorldAction.id)).toBe(helloWorldAction);
|
||||
});
|
||||
|
||||
test('getTriggerCompatibleActions returns attached actions', async () => {
|
||||
|
@ -200,7 +178,7 @@ describe('UiActionsService', () => {
|
|||
title: 'My trigger',
|
||||
};
|
||||
service.registerTrigger(testTrigger);
|
||||
service.addTriggerAction(MY_TRIGGER, helloWorldAction);
|
||||
service.attachAction(MY_TRIGGER, helloWorldAction);
|
||||
|
||||
const compatibleActions = await service.getTriggerCompatibleActions(MY_TRIGGER, {
|
||||
hi: 'there',
|
||||
|
@ -226,7 +204,7 @@ describe('UiActionsService', () => {
|
|||
};
|
||||
|
||||
service.registerTrigger(testTrigger);
|
||||
service.addTriggerAction(testTrigger.id, action);
|
||||
service.attachAction(testTrigger.id, action);
|
||||
|
||||
const compatibleActions1 = await service.getTriggerCompatibleActions(testTrigger.id, {
|
||||
accept: true,
|
||||
|
@ -310,7 +288,7 @@ describe('UiActionsService', () => {
|
|||
id: FOO_TRIGGER,
|
||||
});
|
||||
service1.registerAction(testAction1);
|
||||
service1.addTriggerAction(FOO_TRIGGER, testAction1);
|
||||
service1.attachAction(FOO_TRIGGER, testAction1);
|
||||
|
||||
const service2 = service1.fork();
|
||||
|
||||
|
@ -331,14 +309,14 @@ describe('UiActionsService', () => {
|
|||
});
|
||||
service1.registerAction(testAction1);
|
||||
service1.registerAction(testAction2);
|
||||
service1.addTriggerAction(FOO_TRIGGER, testAction1);
|
||||
service1.attachAction(FOO_TRIGGER, testAction1);
|
||||
|
||||
const service2 = service1.fork();
|
||||
|
||||
expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1);
|
||||
expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1);
|
||||
|
||||
service2.addTriggerAction(FOO_TRIGGER, testAction2);
|
||||
service2.attachAction(FOO_TRIGGER, testAction2);
|
||||
|
||||
expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1);
|
||||
expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(2);
|
||||
|
@ -352,14 +330,14 @@ describe('UiActionsService', () => {
|
|||
});
|
||||
service1.registerAction(testAction1);
|
||||
service1.registerAction(testAction2);
|
||||
service1.addTriggerAction(FOO_TRIGGER, testAction1);
|
||||
service1.attachAction(FOO_TRIGGER, testAction1);
|
||||
|
||||
const service2 = service1.fork();
|
||||
|
||||
expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1);
|
||||
expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1);
|
||||
|
||||
service1.addTriggerAction(FOO_TRIGGER, testAction2);
|
||||
service1.attachAction(FOO_TRIGGER, testAction2);
|
||||
|
||||
expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(2);
|
||||
expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1);
|
||||
|
@ -414,7 +392,7 @@ describe('UiActionsService', () => {
|
|||
} as any;
|
||||
|
||||
service.registerTrigger(trigger);
|
||||
service.addTriggerAction(MY_TRIGGER, action);
|
||||
service.attachAction(MY_TRIGGER, action);
|
||||
|
||||
const actions = service.getTriggerActions(trigger.id);
|
||||
|
||||
|
@ -422,7 +400,7 @@ describe('UiActionsService', () => {
|
|||
expect(actions[0].id).toBe(ACTION_HELLO_WORLD);
|
||||
});
|
||||
|
||||
test('can detach an action from a trigger', () => {
|
||||
test('can detach an action to a trigger', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const trigger: Trigger = {
|
||||
|
@ -435,7 +413,7 @@ describe('UiActionsService', () => {
|
|||
|
||||
service.registerTrigger(trigger);
|
||||
service.registerAction(action);
|
||||
service.addTriggerAction(trigger.id, action);
|
||||
service.attachAction(trigger.id, action);
|
||||
service.detachAction(trigger.id, action.id);
|
||||
|
||||
const actions2 = service.getTriggerActions(trigger.id);
|
||||
|
@ -467,7 +445,7 @@ describe('UiActionsService', () => {
|
|||
} as any;
|
||||
|
||||
service.registerAction(action);
|
||||
expect(() => service.addTriggerAction('i do not exist' as TriggerId, action)).toThrowError(
|
||||
expect(() => service.attachAction('i do not exist' as TriggerId, action)).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = ACTION_HELLO_WORLD].'
|
||||
);
|
||||
});
|
||||
|
@ -497,64 +475,4 @@ describe('UiActionsService', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action factories', () => {
|
||||
const factoryDefinition1: ActionFactoryDefinition = {
|
||||
id: 'test-factory-1',
|
||||
CollectConfig: {} as any,
|
||||
createConfig: () => ({}),
|
||||
isConfigValid: () => true,
|
||||
create: () => ({} as any),
|
||||
};
|
||||
const factoryDefinition2: ActionFactoryDefinition = {
|
||||
id: 'test-factory-2',
|
||||
CollectConfig: {} as any,
|
||||
createConfig: () => ({}),
|
||||
isConfigValid: () => true,
|
||||
create: () => ({} as any),
|
||||
};
|
||||
|
||||
test('.getActionFactories() returns empty array if no action factories registered', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const factories = service.getActionFactories();
|
||||
|
||||
expect(factories).toEqual([]);
|
||||
});
|
||||
|
||||
test('can register and retrieve an action factory', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
service.registerActionFactory(factoryDefinition1);
|
||||
|
||||
const factory = service.getActionFactory(factoryDefinition1.id);
|
||||
|
||||
expect(factory).toBeInstanceOf(ActionFactory);
|
||||
expect(factory.id).toBe(factoryDefinition1.id);
|
||||
});
|
||||
|
||||
test('can retrieve all action factories', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
service.registerActionFactory(factoryDefinition1);
|
||||
service.registerActionFactory(factoryDefinition2);
|
||||
|
||||
const factories = service.getActionFactories();
|
||||
const factoriesSorted = [...factories].sort((f1, f2) => (f1.id > f2.id ? 1 : -1));
|
||||
|
||||
expect(factoriesSorted.length).toBe(2);
|
||||
expect(factoriesSorted[0].id).toBe(factoryDefinition1.id);
|
||||
expect(factoriesSorted[1].id).toBe(factoryDefinition2.id);
|
||||
});
|
||||
|
||||
test('throws when retrieving action factory that does not exist', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
service.registerActionFactory(factoryDefinition1);
|
||||
|
||||
expect(() => service.getActionFactory('UNKNOWN_ID')).toThrowError(
|
||||
'Action factory [actionFactoryId = UNKNOWN_ID] does not exist.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,17 +24,8 @@ import {
|
|||
TriggerId,
|
||||
TriggerContextMapping,
|
||||
ActionType,
|
||||
ActionFactoryRegistry,
|
||||
} from '../types';
|
||||
import {
|
||||
ActionInternal,
|
||||
Action,
|
||||
ActionByType,
|
||||
ActionFactory,
|
||||
ActionDefinition,
|
||||
ActionFactoryDefinition,
|
||||
ActionContext,
|
||||
} from '../actions';
|
||||
import { Action, ActionByType } from '../actions';
|
||||
import { Trigger, TriggerContext } from '../triggers/trigger';
|
||||
import { TriggerInternal } from '../triggers/trigger_internal';
|
||||
import { TriggerContract } from '../triggers/trigger_contract';
|
||||
|
@ -47,25 +38,21 @@ export interface UiActionsServiceParams {
|
|||
* A 1-to-N mapping from `Trigger` to zero or more `Action`.
|
||||
*/
|
||||
readonly triggerToActions?: TriggerToActionsRegistry;
|
||||
readonly actionFactories?: ActionFactoryRegistry;
|
||||
}
|
||||
|
||||
export class UiActionsService {
|
||||
protected readonly triggers: TriggerRegistry;
|
||||
protected readonly actions: ActionRegistry;
|
||||
protected readonly triggerToActions: TriggerToActionsRegistry;
|
||||
protected readonly actionFactories: ActionFactoryRegistry;
|
||||
|
||||
constructor({
|
||||
triggers = new Map(),
|
||||
actions = new Map(),
|
||||
triggerToActions = new Map(),
|
||||
actionFactories = new Map(),
|
||||
}: UiActionsServiceParams = {}) {
|
||||
this.triggers = triggers;
|
||||
this.actions = actions;
|
||||
this.triggerToActions = triggerToActions;
|
||||
this.actionFactories = actionFactories;
|
||||
}
|
||||
|
||||
public readonly registerTrigger = (trigger: Trigger) => {
|
||||
|
@ -89,44 +76,49 @@ export class UiActionsService {
|
|||
return trigger.contract;
|
||||
};
|
||||
|
||||
public readonly registerAction = <A extends ActionDefinition>(
|
||||
definition: A
|
||||
): ActionInternal<A> => {
|
||||
if (this.actions.has(definition.id)) {
|
||||
throw new Error(`Action [action.id = ${definition.id}] already registered.`);
|
||||
public readonly registerAction = <T extends ActionType>(action: ActionByType<T>) => {
|
||||
if (this.actions.has(action.id)) {
|
||||
throw new Error(`Action [action.id = ${action.id}] already registered.`);
|
||||
}
|
||||
|
||||
const action = new ActionInternal(definition);
|
||||
|
||||
this.actions.set(action.id, action);
|
||||
|
||||
return action;
|
||||
};
|
||||
|
||||
public readonly unregisterAction = (actionId: string): void => {
|
||||
if (!this.actions.has(actionId)) {
|
||||
throw new Error(`Action [action.id = ${actionId}] is not registered.`);
|
||||
public readonly getAction = <T extends ActionType>(id: string): ActionByType<T> => {
|
||||
if (!this.actions.has(id)) {
|
||||
throw new Error(`Action [action.id = ${id}] not registered.`);
|
||||
}
|
||||
|
||||
this.actions.delete(actionId);
|
||||
return this.actions.get(id) as ActionByType<T>;
|
||||
};
|
||||
|
||||
public readonly attachAction = <TriggerId extends keyof TriggerContextMapping>(
|
||||
triggerId: TriggerId,
|
||||
actionId: string
|
||||
public readonly attachAction = <TType extends TriggerId, AType extends ActionType>(
|
||||
triggerId: TType,
|
||||
// The action can accept partial or no context, but if it needs context not provided
|
||||
// by this type of trigger, typescript will complain. yay!
|
||||
action: ActionByType<AType> & Action<TriggerContextMapping[TType]>
|
||||
): void => {
|
||||
if (!this.actions.has(action.id)) {
|
||||
this.registerAction(action);
|
||||
} else {
|
||||
const registeredAction = this.actions.get(action.id);
|
||||
if (registeredAction !== action) {
|
||||
throw new Error(`A different action instance with this id is already registered.`);
|
||||
}
|
||||
}
|
||||
|
||||
const trigger = this.triggers.get(triggerId);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error(
|
||||
`No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${actionId}].`
|
||||
`No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${action.id}].`
|
||||
);
|
||||
}
|
||||
|
||||
const actionIds = this.triggerToActions.get(triggerId);
|
||||
|
||||
if (!actionIds!.find(id => id === actionId)) {
|
||||
this.triggerToActions.set(triggerId, [...actionIds!, actionId]);
|
||||
if (!actionIds!.find(id => id === action.id)) {
|
||||
this.triggerToActions.set(triggerId, [...actionIds!, action.id]);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -147,26 +139,6 @@ export class UiActionsService {
|
|||
);
|
||||
};
|
||||
|
||||
public readonly addTriggerAction = <TType extends TriggerId, AType extends ActionType>(
|
||||
triggerId: TType,
|
||||
// The action can accept partial or no context, but if it needs context not provided
|
||||
// by this type of trigger, typescript will complain. yay!
|
||||
action: ActionByType<AType> & Action<TriggerContextMapping[TType]>
|
||||
): void => {
|
||||
if (!this.actions.has(action.id)) this.registerAction(action);
|
||||
this.attachAction(triggerId, action.id);
|
||||
};
|
||||
|
||||
public readonly getAction = <T extends ActionDefinition>(
|
||||
id: string
|
||||
): Action<ActionContext<T>> => {
|
||||
if (!this.actions.has(id)) {
|
||||
throw new Error(`Action [action.id = ${id}] not registered.`);
|
||||
}
|
||||
|
||||
return this.actions.get(id) as ActionInternal<T>;
|
||||
};
|
||||
|
||||
public readonly getTriggerActions = <T extends TriggerId>(
|
||||
triggerId: T
|
||||
): Array<Action<TriggerContextMapping[T]>> => {
|
||||
|
@ -175,9 +147,9 @@ 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.actions.get(actionId)).filter(Boolean) as Array<
|
||||
Action<TriggerContextMapping[T]>
|
||||
>;
|
||||
|
||||
return actions as Array<Action<TriggerContext<T>>>;
|
||||
};
|
||||
|
@ -215,7 +187,6 @@ export class UiActionsService {
|
|||
this.actions.clear();
|
||||
this.triggers.clear();
|
||||
this.triggerToActions.clear();
|
||||
this.actionFactories.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -235,41 +206,4 @@ export class UiActionsService {
|
|||
|
||||
return new UiActionsService({ triggers, actions, triggerToActions });
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an action factory. Action factories are used to configure and
|
||||
* serialize/deserialize dynamic actions.
|
||||
*/
|
||||
public readonly registerActionFactory = <
|
||||
Config extends object = object,
|
||||
FactoryContext extends object = object,
|
||||
ActionContext extends object = object
|
||||
>(
|
||||
definition: ActionFactoryDefinition<Config, FactoryContext, ActionContext>
|
||||
) => {
|
||||
if (this.actionFactories.has(definition.id)) {
|
||||
throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`);
|
||||
}
|
||||
|
||||
const actionFactory = new ActionFactory<Config, FactoryContext, ActionContext>(definition);
|
||||
|
||||
this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory<any, any, any>);
|
||||
};
|
||||
|
||||
public readonly getActionFactory = (actionFactoryId: string): ActionFactory => {
|
||||
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.
|
||||
*/
|
||||
public readonly getActionFactories = (): ActionFactory[] => {
|
||||
return [...this.actionFactories.values()];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ test('executes a single action mapped to a trigger', async () => {
|
|||
const action = createTestAction('test1', () => true);
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.addTriggerAction(trigger.id, action);
|
||||
setup.attachAction(trigger.id, action);
|
||||
|
||||
const context = {};
|
||||
const start = doStart();
|
||||
|
@ -109,7 +109,7 @@ test('does not execute an incompatible action', async () => {
|
|||
);
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.addTriggerAction(trigger.id, action);
|
||||
setup.attachAction(trigger.id, action);
|
||||
|
||||
const start = doStart();
|
||||
const context = {
|
||||
|
@ -130,8 +130,8 @@ test('shows a context menu when more than one action is mapped to a trigger', as
|
|||
const action2 = createTestAction('test2', () => true);
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.addTriggerAction(trigger.id, action1);
|
||||
setup.addTriggerAction(trigger.id, action2);
|
||||
setup.attachAction(trigger.id, action1);
|
||||
setup.attachAction(trigger.id, action2);
|
||||
|
||||
expect(openContextMenu).toHaveBeenCalledTimes(0);
|
||||
|
||||
|
@ -155,7 +155,7 @@ test('passes whole action context to isCompatible()', async () => {
|
|||
});
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.addTriggerAction(trigger.id, action);
|
||||
setup.attachAction(trigger.id, action);
|
||||
|
||||
const start = doStart();
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ActionInternal, Action } from '../actions';
|
||||
import { Action } from '../actions';
|
||||
import { uiActionsPluginMock } from '../mocks';
|
||||
import { TriggerId, ActionType } from '../types';
|
||||
|
||||
|
@ -47,14 +47,13 @@ test('returns actions set on trigger', () => {
|
|||
|
||||
expect(list0).toHaveLength(0);
|
||||
|
||||
setup.addTriggerAction('trigger' as TriggerId, action1);
|
||||
setup.attachAction('trigger' as TriggerId, action1);
|
||||
const list1 = start.getTriggerActions('trigger' as TriggerId);
|
||||
|
||||
expect(list1).toHaveLength(1);
|
||||
expect(list1[0]).toBeInstanceOf(ActionInternal);
|
||||
expect(list1[0].id).toBe(action1.id);
|
||||
expect(list1).toEqual([action1]);
|
||||
|
||||
setup.addTriggerAction('trigger' as TriggerId, action2);
|
||||
setup.attachAction('trigger' as TriggerId, action2);
|
||||
const list2 = start.getTriggerActions('trigger' as TriggerId);
|
||||
|
||||
expect(list2).toHaveLength(2);
|
||||
|
|
|
@ -37,7 +37,7 @@ beforeEach(() => {
|
|||
id: 'trigger' as TriggerId,
|
||||
title: 'trigger',
|
||||
});
|
||||
uiActions.setup.addTriggerAction('trigger' as TriggerId, action);
|
||||
uiActions.setup.attachAction('trigger' as TriggerId, action);
|
||||
});
|
||||
|
||||
test('can register action', async () => {
|
||||
|
@ -58,7 +58,7 @@ test('getTriggerCompatibleActions returns attached actions', async () => {
|
|||
title: 'My trigger',
|
||||
};
|
||||
setup.registerTrigger(testTrigger);
|
||||
setup.addTriggerAction('MY-TRIGGER' as TriggerId, helloWorldAction);
|
||||
setup.attachAction('MY-TRIGGER' as TriggerId, helloWorldAction);
|
||||
|
||||
const start = doStart();
|
||||
const actions = await start.getTriggerCompatibleActions('MY-TRIGGER' as TriggerId, {});
|
||||
|
@ -84,7 +84,7 @@ test('filters out actions not applicable based on the context', async () => {
|
|||
|
||||
setup.registerTrigger(testTrigger);
|
||||
setup.registerAction(action1);
|
||||
setup.addTriggerAction(testTrigger.id, action1);
|
||||
setup.attachAction(testTrigger.id, action1);
|
||||
|
||||
const start = doStart();
|
||||
let actions = await start.getTriggerCompatibleActions(testTrigger.id, { accept: true });
|
||||
|
|
|
@ -16,5 +16,4 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { createHelloWorldAction } from './hello_world_action';
|
||||
|
|
|
@ -22,6 +22,6 @@ import { Trigger } from '.';
|
|||
export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER';
|
||||
export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = {
|
||||
id: SELECT_RANGE_TRIGGER,
|
||||
title: '',
|
||||
title: 'Select range',
|
||||
description: 'Applies a range filter',
|
||||
};
|
||||
|
|
|
@ -72,7 +72,6 @@ export class TriggerInternal<T extends TriggerId> {
|
|||
const panel = await buildContextMenuForActions({
|
||||
actions,
|
||||
actionContext: context,
|
||||
title: this.trigger.title,
|
||||
closeMenu: () => session.close(),
|
||||
});
|
||||
const session = openContextMenu([panel]);
|
||||
|
|
|
@ -22,6 +22,6 @@ import { Trigger } from '.';
|
|||
export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER';
|
||||
export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = {
|
||||
id: VALUE_CLICK_TRIGGER,
|
||||
title: '',
|
||||
title: 'Value clicked',
|
||||
description: 'Value was clicked',
|
||||
};
|
||||
|
|
|
@ -17,17 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ActionInternal } from './actions/action_internal';
|
||||
import { ActionByType } from './actions/action';
|
||||
import { TriggerInternal } from './triggers/trigger_internal';
|
||||
import { ActionFactory } from './actions';
|
||||
import { EmbeddableVisTriggerContext, IEmbeddable } from '../../embeddable/public';
|
||||
import { Filter } from '../../data/public';
|
||||
import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers';
|
||||
|
||||
export type TriggerRegistry = Map<TriggerId, TriggerInternal<any>>;
|
||||
export type ActionRegistry = Map<string, ActionInternal>;
|
||||
export type ActionRegistry = Map<string, ActionByType<any>>;
|
||||
export type TriggerToActionsRegistry = Map<TriggerId, string[]>;
|
||||
export type ActionFactoryRegistry = Map<string, ActionFactory>;
|
||||
|
||||
const DEFAULT_TRIGGER = '';
|
||||
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiComponent } from 'src/plugins/kibana_utils/common';
|
||||
|
||||
/**
|
||||
* Represents something that can be configured by user using UI.
|
||||
*/
|
||||
export interface Configurable<Config extends object = object, Context = object> {
|
||||
/**
|
||||
* Create default config for this item, used when item is created for the first time.
|
||||
*/
|
||||
readonly createConfig: () => Config;
|
||||
|
||||
/**
|
||||
* Is this config valid. Used to validate user's input before saving.
|
||||
*/
|
||||
readonly isConfigValid: (config: Config) => boolean;
|
||||
|
||||
/**
|
||||
* `UiComponent` to be rendered when collecting configuration for this item.
|
||||
*/
|
||||
readonly CollectConfig: UiComponent<CollectConfigProps<Config, Context>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props provided to `CollectConfig` component on every re-render.
|
||||
*/
|
||||
export interface CollectConfigProps<Config extends object = object, Context = object> {
|
||||
/**
|
||||
* Current (latest) config of the item.
|
||||
*/
|
||||
config: Config;
|
||||
|
||||
/**
|
||||
* Callback called when user updates the config in UI.
|
||||
*/
|
||||
onConfig: (config: Config) => void;
|
||||
|
||||
/**
|
||||
* Context information about where component is being rendered.
|
||||
*/
|
||||
context: Context;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './presentable';
|
||||
export * from './configurable';
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
// eslint-disable-next-line
|
||||
require('@kbn/storybook').runStorybookCli({
|
||||
name: 'ui_actions',
|
||||
storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')],
|
||||
});
|
|
@ -70,10 +70,11 @@ export class EmbeddableExplorerPublicPlugin
|
|||
const sayHelloAction = new SayHelloAction(alert);
|
||||
const sendMessageAction = createSendMessageAction(core.overlays);
|
||||
|
||||
plugins.uiActions.registerAction(helloWorldAction);
|
||||
plugins.uiActions.registerAction(sayHelloAction);
|
||||
plugins.uiActions.registerAction(sendMessageAction);
|
||||
|
||||
plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, helloWorldAction);
|
||||
plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction);
|
||||
|
||||
plugins.__LEGACY.onRenderComplete(() => {
|
||||
const root = document.getElementById(REACT_ROOT_ID);
|
||||
|
|
|
@ -62,4 +62,5 @@ function createSamplePanelAction() {
|
|||
}
|
||||
|
||||
const action = createSamplePanelAction();
|
||||
npSetup.plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action);
|
||||
npSetup.plugins.uiActions.registerAction(action);
|
||||
npSetup.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action);
|
||||
|
|
|
@ -33,4 +33,5 @@ export const createSamplePanelLink = (): Action =>
|
|||
});
|
||||
|
||||
const action = createSamplePanelLink();
|
||||
npStart.plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action);
|
||||
npStart.plugins.uiActions.registerAction(action);
|
||||
npStart.plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, action);
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
"xpack.beatsManagement": "legacy/plugins/beats_management",
|
||||
"xpack.canvas": "legacy/plugins/canvas",
|
||||
"xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication",
|
||||
"xpack.dashboard": "plugins/dashboard_enhanced",
|
||||
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
|
||||
"xpack.data": "plugins/data_enhanced",
|
||||
"xpack.drilldowns": "plugins/drilldowns",
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
.auaActionWizard__selectedActionFactoryContainer {
|
||||
background-color: $euiColorLightestShade;
|
||||
padding: $euiSize;
|
||||
}
|
||||
|
||||
.auaActionWizard__actionFactoryItem {
|
||||
.euiKeyPadMenuItem__label {
|
||||
height: #{$euiSizeXL};
|
||||
|
|
|
@ -6,26 +6,28 @@
|
|||
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { Demo, dashboardFactory, urlFactory } from './test_data';
|
||||
import { dashboardDrilldownActionFactory, Demo, urlDrilldownActionFactory } from './test_data';
|
||||
|
||||
storiesOf('components/ActionWizard', module)
|
||||
.add('default', () => <Demo actionFactories={[dashboardFactory, urlFactory]} />)
|
||||
.add('default', () => (
|
||||
<Demo actionFactories={[dashboardDrilldownActionFactory, urlDrilldownActionFactory]} />
|
||||
))
|
||||
.add('Only one factory is available', () => (
|
||||
// to make sure layout doesn't break
|
||||
<Demo actionFactories={[dashboardFactory]} />
|
||||
<Demo actionFactories={[dashboardDrilldownActionFactory]} />
|
||||
))
|
||||
.add('Long list of action factories', () => (
|
||||
// to make sure layout doesn't break
|
||||
<Demo
|
||||
actionFactories={[
|
||||
dashboardFactory,
|
||||
urlFactory,
|
||||
dashboardFactory,
|
||||
urlFactory,
|
||||
dashboardFactory,
|
||||
urlFactory,
|
||||
dashboardFactory,
|
||||
urlFactory,
|
||||
dashboardDrilldownActionFactory,
|
||||
urlDrilldownActionFactory,
|
||||
dashboardDrilldownActionFactory,
|
||||
urlDrilldownActionFactory,
|
||||
dashboardDrilldownActionFactory,
|
||||
urlDrilldownActionFactory,
|
||||
dashboardDrilldownActionFactory,
|
||||
urlDrilldownActionFactory,
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -8,14 +8,21 @@ import React from 'react';
|
|||
import { cleanup, fireEvent, render } from '@testing-library/react/pure';
|
||||
import '@testing-library/jest-dom/extend-expect'; // TODO: this should be global
|
||||
import { TEST_SUBJ_ACTION_FACTORY_ITEM, TEST_SUBJ_SELECTED_ACTION_FACTORY } from './action_wizard';
|
||||
import { dashboardFactory, dashboards, Demo, urlFactory } from './test_data';
|
||||
import {
|
||||
dashboardDrilldownActionFactory,
|
||||
dashboards,
|
||||
Demo,
|
||||
urlDrilldownActionFactory,
|
||||
} from './test_data';
|
||||
|
||||
// TODO: afterEach is not available for it globally during setup
|
||||
// https://github.com/elastic/kibana/issues/59469
|
||||
afterEach(cleanup);
|
||||
|
||||
test('Pick and configure action', () => {
|
||||
const screen = render(<Demo actionFactories={[dashboardFactory, urlFactory]} />);
|
||||
const screen = render(
|
||||
<Demo actionFactories={[dashboardDrilldownActionFactory, urlDrilldownActionFactory]} />
|
||||
);
|
||||
|
||||
// check that all factories are displayed to pick
|
||||
expect(screen.getAllByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).toHaveLength(2);
|
||||
|
@ -40,7 +47,7 @@ test('Pick and configure action', () => {
|
|||
});
|
||||
|
||||
test('If only one actions factory is available then actionFactory selection is emitted without user input', () => {
|
||||
const screen = render(<Demo actionFactories={[urlFactory]} />);
|
||||
const screen = render(<Demo actionFactories={[urlDrilldownActionFactory]} />);
|
||||
|
||||
// check that no factories are displayed to pick from
|
||||
expect(screen.queryByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).not.toBeInTheDocument();
|
||||
|
|
|
@ -16,23 +16,40 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { txtChangeButton } from './i18n';
|
||||
import './action_wizard.scss';
|
||||
import { ActionFactory } from '../../services';
|
||||
|
||||
type ActionBaseConfig = object;
|
||||
type ActionFactoryBaseContext = object;
|
||||
// TODO: this interface is temporary for just moving forward with the component
|
||||
// and it will be imported from the ../ui_actions when implemented properly
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type ActionBaseConfig = {};
|
||||
export interface ActionFactory<Config extends ActionBaseConfig = ActionBaseConfig> {
|
||||
type: string; // TODO: type should be tied to Action and ActionByType
|
||||
displayName: string;
|
||||
iconType?: string;
|
||||
wizard: React.FC<ActionFactoryWizardProps<Config>>;
|
||||
createConfig: () => Config;
|
||||
isValid: (config: Config) => boolean;
|
||||
}
|
||||
|
||||
export interface ActionFactoryWizardProps<Config extends ActionBaseConfig> {
|
||||
config?: Config;
|
||||
|
||||
/**
|
||||
* Callback called when user updates the config in UI.
|
||||
*/
|
||||
onConfig: (config: Config) => void;
|
||||
}
|
||||
|
||||
export interface ActionWizardProps {
|
||||
/**
|
||||
* List of available action factories
|
||||
*/
|
||||
actionFactories: ActionFactory[];
|
||||
actionFactories: Array<ActionFactory<any>>; // any here to be able to pass array of ActionFactory<Config> with different configs
|
||||
|
||||
/**
|
||||
* Currently selected action factory
|
||||
* undefined - is allowed and means that non is selected
|
||||
*/
|
||||
currentActionFactory?: ActionFactory;
|
||||
|
||||
/**
|
||||
* Action factory selected changed
|
||||
* null - means user click "change" and removed action factory selection
|
||||
|
@ -48,11 +65,6 @@ export interface ActionWizardProps {
|
|||
* config changed
|
||||
*/
|
||||
onConfigChange: (config: ActionBaseConfig) => void;
|
||||
|
||||
/**
|
||||
* Context will be passed into ActionFactory's methods
|
||||
*/
|
||||
context: ActionFactoryBaseContext;
|
||||
}
|
||||
|
||||
export const ActionWizard: React.FC<ActionWizardProps> = ({
|
||||
|
@ -61,7 +73,6 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
onActionFactoryChange,
|
||||
onConfigChange,
|
||||
config,
|
||||
context,
|
||||
}) => {
|
||||
// auto pick action factory if there is only 1 available
|
||||
if (!currentActionFactory && actionFactories.length === 1) {
|
||||
|
@ -76,7 +87,6 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
onDeselect={() => {
|
||||
onActionFactoryChange(null);
|
||||
}}
|
||||
context={context}
|
||||
config={config}
|
||||
onConfigChange={newConfig => {
|
||||
onConfigChange(newConfig);
|
||||
|
@ -87,7 +97,6 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
|
||||
return (
|
||||
<ActionFactorySelector
|
||||
context={context}
|
||||
actionFactories={actionFactories}
|
||||
onActionFactorySelected={actionFactory => {
|
||||
onActionFactoryChange(actionFactory);
|
||||
|
@ -96,11 +105,10 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
interface SelectedActionFactoryProps {
|
||||
actionFactory: ActionFactory;
|
||||
config: ActionBaseConfig;
|
||||
context: ActionFactoryBaseContext;
|
||||
onConfigChange: (config: ActionBaseConfig) => void;
|
||||
interface SelectedActionFactoryProps<Config extends ActionBaseConfig = ActionBaseConfig> {
|
||||
actionFactory: ActionFactory<Config>;
|
||||
config: Config;
|
||||
onConfigChange: (config: Config) => void;
|
||||
showDeselect: boolean;
|
||||
onDeselect: () => void;
|
||||
}
|
||||
|
@ -113,28 +121,28 @@ const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({
|
|||
showDeselect,
|
||||
onConfigChange,
|
||||
config,
|
||||
context,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="auaActionWizard__selectedActionFactoryContainer"
|
||||
data-test-subj={TEST_SUBJ_SELECTED_ACTION_FACTORY}
|
||||
data-testid={TEST_SUBJ_SELECTED_ACTION_FACTORY}
|
||||
>
|
||||
<header>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
{actionFactory.getIconType(context) && (
|
||||
{actionFactory.iconType && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={actionFactory.getIconType(context)!} size="m" />
|
||||
<EuiIcon type={actionFactory.iconType} size="m" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiText>
|
||||
<h4>{actionFactory.getDisplayName(context)}</h4>
|
||||
<h4>{actionFactory.displayName}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{showDeselect && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty size="xs" onClick={() => onDeselect()}>
|
||||
<EuiButtonEmpty size="s" onClick={() => onDeselect()}>
|
||||
{txtChangeButton}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
@ -143,11 +151,10 @@ const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({
|
|||
</header>
|
||||
<EuiSpacer size="m" />
|
||||
<div>
|
||||
<actionFactory.ReactCollectConfig
|
||||
config={config}
|
||||
onConfig={onConfigChange}
|
||||
context={context}
|
||||
/>
|
||||
{actionFactory.wizard({
|
||||
config,
|
||||
onConfig: onConfigChange,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -155,7 +162,6 @@ const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({
|
|||
|
||||
interface ActionFactorySelectorProps {
|
||||
actionFactories: ActionFactory[];
|
||||
context: ActionFactoryBaseContext;
|
||||
onActionFactorySelected: (actionFactory: ActionFactory) => void;
|
||||
}
|
||||
|
||||
|
@ -164,7 +170,6 @@ export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'action-factory-item';
|
|||
const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
|
||||
actionFactories,
|
||||
onActionFactorySelected,
|
||||
context,
|
||||
}) => {
|
||||
if (actionFactories.length === 0) {
|
||||
// this is not user facing, as it would be impossible to get into this state
|
||||
|
@ -173,22 +178,18 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" wrap={true}>
|
||||
{[...actionFactories]
|
||||
.sort((f1, f2) => f1.order - f2.order)
|
||||
.map(actionFactory => (
|
||||
<EuiFlexItem grow={false} key={actionFactory.id}>
|
||||
<EuiFlexGroup wrap>
|
||||
{actionFactories.map(actionFactory => (
|
||||
<EuiKeyPadMenuItemButton
|
||||
className="auaActionWizard__actionFactoryItem"
|
||||
label={actionFactory.getDisplayName(context)}
|
||||
key={actionFactory.type}
|
||||
label={actionFactory.displayName}
|
||||
data-testid={TEST_SUBJ_ACTION_FACTORY_ITEM}
|
||||
data-test-subj={TEST_SUBJ_ACTION_FACTORY_ITEM}
|
||||
onClick={() => onActionFactorySelected(actionFactory)}
|
||||
>
|
||||
{actionFactory.getIconType(context) && (
|
||||
<EuiIcon type={actionFactory.getIconType(context)!} size="m" />
|
||||
)}
|
||||
{actionFactory.iconType && <EuiIcon type={actionFactory.iconType} size="m" />}
|
||||
</EuiKeyPadMenuItemButton>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,6 @@ import { i18n } from '@kbn/i18n';
|
|||
export const txtChangeButton = i18n.translate(
|
||||
'xpack.advancedUiActions.components.actionWizard.changeButton',
|
||||
{
|
||||
defaultMessage: 'Change',
|
||||
defaultMessage: 'change',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ActionWizard } from './action_wizard';
|
||||
export { ActionFactory, ActionWizard } from './action_wizard';
|
||||
|
|
|
@ -6,29 +6,37 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui';
|
||||
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { ActionWizard } from './action_wizard';
|
||||
import { ActionFactoryDefinition, ActionFactory } from '../../services';
|
||||
import { CollectConfigProps } from '../../util';
|
||||
|
||||
type ActionBaseConfig = object;
|
||||
import { ActionFactory, ActionBaseConfig, ActionWizard } from './action_wizard';
|
||||
|
||||
export const dashboards = [
|
||||
{ id: 'dashboard1', title: 'Dashboard 1' },
|
||||
{ id: 'dashboard2', title: 'Dashboard 2' },
|
||||
];
|
||||
|
||||
interface DashboardDrilldownConfig {
|
||||
export const dashboardDrilldownActionFactory: ActionFactory<{
|
||||
dashboardId?: string;
|
||||
useCurrentFilters: boolean;
|
||||
useCurrentDateRange: boolean;
|
||||
}
|
||||
|
||||
function DashboardDrilldownCollectConfig(props: CollectConfigProps<DashboardDrilldownConfig>) {
|
||||
useCurrentDashboardFilters: boolean;
|
||||
useCurrentDashboardDataRange: boolean;
|
||||
}> = {
|
||||
type: 'Dashboard',
|
||||
displayName: 'Go to Dashboard',
|
||||
iconType: 'dashboardApp',
|
||||
createConfig: () => {
|
||||
return {
|
||||
dashboardId: undefined,
|
||||
useCurrentDashboardDataRange: true,
|
||||
useCurrentDashboardFilters: true,
|
||||
};
|
||||
},
|
||||
isValid: config => {
|
||||
if (!config.dashboardId) return false;
|
||||
return true;
|
||||
},
|
||||
wizard: props => {
|
||||
const config = props.config ?? {
|
||||
dashboardId: undefined,
|
||||
useCurrentFilters: true,
|
||||
useCurrentDateRange: true,
|
||||
useCurrentDashboardDataRange: true,
|
||||
useCurrentDashboardFilters: true,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
@ -47,11 +55,11 @@ function DashboardDrilldownCollectConfig(props: CollectConfigProps<DashboardDril
|
|||
<EuiSwitch
|
||||
name="useCurrentFilters"
|
||||
label="Use current dashboard's filters"
|
||||
checked={config.useCurrentFilters}
|
||||
checked={config.useCurrentDashboardFilters}
|
||||
onChange={() =>
|
||||
props.onConfig({
|
||||
...config,
|
||||
useCurrentFilters: !config.useCurrentFilters,
|
||||
useCurrentDashboardFilters: !config.useCurrentDashboardFilters,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
@ -60,57 +68,35 @@ function DashboardDrilldownCollectConfig(props: CollectConfigProps<DashboardDril
|
|||
<EuiSwitch
|
||||
name="useCurrentDateRange"
|
||||
label="Use current dashboard's date range"
|
||||
checked={config.useCurrentDateRange}
|
||||
checked={config.useCurrentDashboardDataRange}
|
||||
onChange={() =>
|
||||
props.onConfig({
|
||||
...config,
|
||||
useCurrentDateRange: !config.useCurrentDateRange,
|
||||
useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const dashboardDrilldownActionFactory: ActionFactoryDefinition<
|
||||
DashboardDrilldownConfig,
|
||||
any,
|
||||
any
|
||||
> = {
|
||||
id: 'Dashboard',
|
||||
getDisplayName: () => 'Go to Dashboard',
|
||||
getIconType: () => 'dashboardApp',
|
||||
export const urlDrilldownActionFactory: ActionFactory<{ url: string; openInNewTab: boolean }> = {
|
||||
type: 'Url',
|
||||
displayName: 'Go to URL',
|
||||
iconType: 'link',
|
||||
createConfig: () => {
|
||||
return {
|
||||
dashboardId: undefined,
|
||||
useCurrentFilters: true,
|
||||
useCurrentDateRange: true,
|
||||
url: '',
|
||||
openInNewTab: false,
|
||||
};
|
||||
},
|
||||
isConfigValid: (config: DashboardDrilldownConfig): config is DashboardDrilldownConfig => {
|
||||
if (!config.dashboardId) return false;
|
||||
isValid: config => {
|
||||
if (!config.url) return false;
|
||||
return true;
|
||||
},
|
||||
CollectConfig: reactToUiComponent(DashboardDrilldownCollectConfig),
|
||||
|
||||
isCompatible(context?: object): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
order: 0,
|
||||
create: () => ({
|
||||
id: 'test',
|
||||
execute: async () => alert('Navigate to dashboard!'),
|
||||
}),
|
||||
};
|
||||
|
||||
export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory);
|
||||
|
||||
interface UrlDrilldownConfig {
|
||||
url: string;
|
||||
openInNewTab: boolean;
|
||||
}
|
||||
function UrlDrilldownCollectConfig(props: CollectConfigProps<UrlDrilldownConfig>) {
|
||||
wizard: props => {
|
||||
const config = props.config ?? {
|
||||
url: '',
|
||||
openInNewTab: false,
|
||||
|
@ -135,31 +121,8 @@ function UrlDrilldownCollectConfig(props: CollectConfigProps<UrlDrilldownConfig>
|
|||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const urlDrilldownActionFactory: ActionFactoryDefinition<UrlDrilldownConfig> = {
|
||||
id: 'Url',
|
||||
getDisplayName: () => 'Go to URL',
|
||||
getIconType: () => 'link',
|
||||
createConfig: () => {
|
||||
return {
|
||||
url: '',
|
||||
openInNewTab: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
isConfigValid: (config: UrlDrilldownConfig): config is UrlDrilldownConfig => {
|
||||
if (!config.url) return false;
|
||||
return true;
|
||||
},
|
||||
CollectConfig: reactToUiComponent(UrlDrilldownCollectConfig),
|
||||
|
||||
order: 10,
|
||||
isCompatible(context?: object): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
create: () => null as any,
|
||||
};
|
||||
|
||||
export const urlFactory = new ActionFactory(urlDrilldownActionFactory);
|
||||
|
||||
export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory<any>> }) {
|
||||
const [state, setState] = useState<{
|
||||
|
@ -194,15 +157,14 @@ export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory
|
|||
changeActionFactory(newActionFactory);
|
||||
}}
|
||||
currentActionFactory={state.currentActionFactory}
|
||||
context={{}}
|
||||
/>
|
||||
<div style={{ marginTop: '44px' }} />
|
||||
<hr />
|
||||
<div>Action Factory Id: {state.currentActionFactory?.id}</div>
|
||||
<div>Action Factory Type: {state.currentActionFactory?.type}</div>
|
||||
<div>Action Factory Config: {JSON.stringify(state.config)}</div>
|
||||
<div>
|
||||
Is config valid:{' '}
|
||||
{JSON.stringify(state.currentActionFactory?.isConfigValid(state.config!) ?? false)}
|
||||
{JSON.stringify(state.currentActionFactory?.isValid(state.config!) ?? false)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -44,7 +44,7 @@ export class CustomTimeRangeAction implements ActionByType<typeof CUSTOM_TIME_RA
|
|||
private dateFormat?: string;
|
||||
private commonlyUsedRanges: CommonlyUsedRange[];
|
||||
public readonly id = CUSTOM_TIME_RANGE;
|
||||
public order = 30;
|
||||
public order = 7;
|
||||
|
||||
constructor({
|
||||
openModal,
|
||||
|
|
|
@ -12,17 +12,3 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
export { AdvancedUiActionsPublicPlugin as Plugin };
|
||||
export {
|
||||
SetupContract as AdvancedUiActionsSetup,
|
||||
StartContract as AdvancedUiActionsStart,
|
||||
} from './plugin';
|
||||
|
||||
export { ActionWizard } from './components';
|
||||
export {
|
||||
ActionFactoryDefinition as AdvancedUiActionsActionFactoryDefinition,
|
||||
ActionFactory as AdvancedUiActionsActionFactory,
|
||||
} from './services/action_factory_service';
|
||||
export {
|
||||
Configurable as AdvancedUiActionsConfigurable,
|
||||
CollectConfigProps as AdvancedUiActionsCollectConfigProps,
|
||||
} from './util';
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
Plugin,
|
||||
} from '../../../../src/core/public';
|
||||
import { createReactOverlays } from '../../../../src/plugins/kibana_react/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
|
||||
import { UiActionsStart, UiActionsSetup } from '../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
PANEL_BADGE_TRIGGER,
|
||||
|
@ -41,10 +41,8 @@ interface StartDependencies {
|
|||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SetupContract extends UiActionsSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface StartContract extends UiActionsStart {}
|
||||
export type Setup = void;
|
||||
export type Start = void;
|
||||
|
||||
declare module '../../../../src/plugins/ui_actions/public' {
|
||||
export interface ActionContextMapping {
|
||||
|
@ -54,16 +52,12 @@ declare module '../../../../src/plugins/ui_actions/public' {
|
|||
}
|
||||
|
||||
export class AdvancedUiActionsPublicPlugin
|
||||
implements Plugin<SetupContract, StartContract, SetupDependencies, StartDependencies> {
|
||||
implements Plugin<Setup, Start, SetupDependencies, StartDependencies> {
|
||||
constructor(initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup, { uiActions }: SetupDependencies): SetupContract {
|
||||
return {
|
||||
...uiActions,
|
||||
};
|
||||
}
|
||||
public setup(core: CoreSetup, { uiActions }: SetupDependencies): Setup {}
|
||||
|
||||
public start(core: CoreStart, { uiActions }: StartDependencies): StartContract {
|
||||
public start(core: CoreStart, { uiActions }: StartDependencies): Start {
|
||||
const dateFormat = core.uiSettings.get('dateFormat') as string;
|
||||
const commonlyUsedRanges = core.uiSettings.get('timepicker:quickRanges') as CommonlyUsedRange[];
|
||||
const { openModal } = createReactOverlays(core);
|
||||
|
@ -72,18 +66,16 @@ export class AdvancedUiActionsPublicPlugin
|
|||
dateFormat,
|
||||
commonlyUsedRanges,
|
||||
});
|
||||
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, timeRangeAction);
|
||||
uiActions.registerAction(timeRangeAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, timeRangeAction);
|
||||
|
||||
const timeRangeBadge = new CustomTimeRangeBadge({
|
||||
openModal,
|
||||
dateFormat,
|
||||
commonlyUsedRanges,
|
||||
});
|
||||
uiActions.addTriggerAction(PANEL_BADGE_TRIGGER, timeRangeBadge);
|
||||
|
||||
return {
|
||||
...uiActions,
|
||||
};
|
||||
uiActions.registerAction(timeRangeBadge);
|
||||
uiActions.attachAction(PANEL_BADGE_TRIGGER, timeRangeBadge);
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export {
|
||||
ActionFactory
|
||||
} from '../../../../../../src/plugins/ui_actions/public/actions/action_factory';
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export {
|
||||
ActionFactoryDefinition
|
||||
} from '../../../../../../src/plugins/ui_actions/public/actions/action_factory_definition';
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './action_factory_definition';
|
||||
export * from './action_factory';
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
UiActionsConfigurable as Configurable,
|
||||
UiActionsCollectConfigProps as CollectConfigProps,
|
||||
} from '../../../../../src/plugins/ui_actions/public';
|
|
@ -1 +0,0 @@
|
|||
# X-Pack part of Dashboard app
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"id": "dashboardEnhanced",
|
||||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["uiActions", "embeddable", "dashboard", "drilldowns"],
|
||||
"configPath": ["xpack", "dashboardEnhanced"]
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
# Presentation React components
|
||||
|
||||
Here we keep reusable *presentation* (aka *dumb*) React components—these
|
||||
components should not be connected to state and ideally should not know anything
|
||||
about Kibana.
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { DashboardDrilldownConfig } from '.';
|
||||
|
||||
export const dashboards = [
|
||||
{ id: 'dashboard1', title: 'Dashboard 1' },
|
||||
{ id: 'dashboard2', title: 'Dashboard 2' },
|
||||
{ id: 'dashboard3', title: 'Dashboard 3' },
|
||||
];
|
||||
|
||||
const InteractiveDemo: React.FC = () => {
|
||||
const [activeDashboardId, setActiveDashboardId] = React.useState('dashboard1');
|
||||
const [currentFilters, setCurrentFilters] = React.useState(false);
|
||||
const [keepRange, setKeepRange] = React.useState(false);
|
||||
|
||||
return (
|
||||
<DashboardDrilldownConfig
|
||||
activeDashboardId={activeDashboardId}
|
||||
dashboards={dashboards}
|
||||
currentFilters={currentFilters}
|
||||
keepRange={keepRange}
|
||||
onDashboardSelect={id => setActiveDashboardId(id)}
|
||||
onCurrentFiltersToggle={() => setCurrentFilters(old => !old)}
|
||||
onKeepRangeToggle={() => setKeepRange(old => !old)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('components/DashboardDrilldownConfig', module)
|
||||
.add('default', () => (
|
||||
<DashboardDrilldownConfig
|
||||
activeDashboardId={'dashboard2'}
|
||||
dashboards={dashboards}
|
||||
onDashboardSelect={e => console.log('onDashboardSelect', e)}
|
||||
/>
|
||||
))
|
||||
.add('with switches', () => (
|
||||
<DashboardDrilldownConfig
|
||||
activeDashboardId={'dashboard2'}
|
||||
dashboards={dashboards}
|
||||
onDashboardSelect={e => console.log('onDashboardSelect', e)}
|
||||
onCurrentFiltersToggle={() => console.log('onCurrentFiltersToggle')}
|
||||
onKeepRangeToggle={() => console.log('onKeepRangeToggle')}
|
||||
/>
|
||||
))
|
||||
.add('interactive demo', () => <InteractiveDemo />);
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
test.todo('renders list of dashboards');
|
||||
test.todo('renders correct selected dashboard');
|
||||
test.todo('can change dashboard');
|
||||
test.todo('can toggle "use current filters" switch');
|
||||
test.todo('can toggle "date range" switch');
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui';
|
||||
import { txtChooseDestinationDashboard } from './i18n';
|
||||
|
||||
export interface DashboardItem {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface DashboardDrilldownConfigProps {
|
||||
activeDashboardId?: string;
|
||||
dashboards: DashboardItem[];
|
||||
currentFilters?: boolean;
|
||||
keepRange?: boolean;
|
||||
onDashboardSelect: (dashboardId: string) => void;
|
||||
onCurrentFiltersToggle?: () => void;
|
||||
onKeepRangeToggle?: () => void;
|
||||
}
|
||||
|
||||
export const DashboardDrilldownConfig: React.FC<DashboardDrilldownConfigProps> = ({
|
||||
activeDashboardId,
|
||||
dashboards,
|
||||
currentFilters,
|
||||
keepRange,
|
||||
onDashboardSelect,
|
||||
onCurrentFiltersToggle,
|
||||
onKeepRangeToggle,
|
||||
}) => {
|
||||
// TODO: use i18n below.
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow label={txtChooseDestinationDashboard}>
|
||||
<EuiSelect
|
||||
name="selectDashboard"
|
||||
hasNoInitialSelection={true}
|
||||
options={dashboards.map(({ id, title }) => ({ value: id, text: title }))}
|
||||
value={activeDashboardId}
|
||||
onChange={e => onDashboardSelect(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{!!onCurrentFiltersToggle && (
|
||||
<EuiFormRow hasChildLabel={false}>
|
||||
<EuiSwitch
|
||||
name="useCurrentFilters"
|
||||
label="Use current dashboard's filters"
|
||||
checked={!!currentFilters}
|
||||
onChange={onCurrentFiltersToggle}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{!!onKeepRangeToggle && (
|
||||
<EuiFormRow hasChildLabel={false}>
|
||||
<EuiSwitch
|
||||
name="useCurrentDateRange"
|
||||
label="Use current dashboard's date range"
|
||||
checked={!!keepRange}
|
||||
onChange={onKeepRangeToggle}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* 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 txtChooseDestinationDashboard = i18n.translate(
|
||||
'xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard',
|
||||
{
|
||||
defaultMessage: 'Choose destination dashboard',
|
||||
}
|
||||
);
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './dashboard_drilldown_config';
|
|
@ -1,7 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './dashboard_drilldown_config';
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from 'src/core/public';
|
||||
import { DashboardEnhancedPlugin } from './plugin';
|
||||
|
||||
export {
|
||||
SetupContract as DashboardEnhancedSetupContract,
|
||||
SetupDependencies as DashboardEnhancedSetupDependencies,
|
||||
StartContract as DashboardEnhancedStartContract,
|
||||
StartDependencies as DashboardEnhancedStartDependencies,
|
||||
} from './plugin';
|
||||
|
||||
export function plugin(context: PluginInitializerContext) {
|
||||
return new DashboardEnhancedPlugin(context);
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* 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 { DashboardEnhancedSetupContract, DashboardEnhancedStartContract } from '.';
|
||||
|
||||
export type Setup = jest.Mocked<DashboardEnhancedSetupContract>;
|
||||
export type Start = jest.Mocked<DashboardEnhancedStartContract>;
|
||||
|
||||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {};
|
||||
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
const createStartContract = (): Start => {
|
||||
const startContract: Start = {};
|
||||
|
||||
return startContract;
|
||||
};
|
||||
|
||||
export const dashboardEnhancedPluginMock = {
|
||||
createSetupContract,
|
||||
createStartContract,
|
||||
};
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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 { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
|
||||
import { DashboardDrilldownsService } from './services';
|
||||
import { DrilldownsSetup, DrilldownsStart } from '../../drilldowns/public';
|
||||
|
||||
export interface SetupDependencies {
|
||||
uiActions: UiActionsSetup;
|
||||
drilldowns: DrilldownsSetup;
|
||||
}
|
||||
|
||||
export interface StartDependencies {
|
||||
uiActions: UiActionsStart;
|
||||
drilldowns: DrilldownsStart;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export interface SetupContract {}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export interface StartContract {}
|
||||
|
||||
export class DashboardEnhancedPlugin
|
||||
implements Plugin<SetupContract, StartContract, SetupDependencies, StartDependencies> {
|
||||
public readonly drilldowns = new DashboardDrilldownsService();
|
||||
public readonly config: { drilldowns: { enabled: boolean } };
|
||||
|
||||
constructor(protected readonly context: PluginInitializerContext) {
|
||||
this.config = context.config.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup<StartDependencies>, plugins: SetupDependencies): SetupContract {
|
||||
this.drilldowns.bootstrap(core, plugins, {
|
||||
enableDrilldowns: this.config.drilldowns.enabled,
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: StartDependencies): StartContract {
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
FlyoutCreateDrilldownAction,
|
||||
OpenFlyoutAddDrilldownParams,
|
||||
} from './flyout_create_drilldown';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import { drilldownsPluginMock } from '../../../../../../drilldowns/public/mocks';
|
||||
import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { uiActionsPluginMock } from '../../../../../../../../src/plugins/ui_actions/public/mocks';
|
||||
import { TriggerContextMapping } from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import { MockEmbeddable } from '../test_helpers';
|
||||
|
||||
const overlays = coreMock.createStart().overlays;
|
||||
const drilldowns = drilldownsPluginMock.createStartContract();
|
||||
const uiActions = uiActionsPluginMock.createStartContract();
|
||||
|
||||
const actionParams: OpenFlyoutAddDrilldownParams = {
|
||||
drilldowns: () => Promise.resolve(drilldowns),
|
||||
overlays: () => Promise.resolve(overlays),
|
||||
};
|
||||
|
||||
test('should create', () => {
|
||||
expect(() => new FlyoutCreateDrilldownAction(actionParams)).not.toThrow();
|
||||
});
|
||||
|
||||
test('title is a string', () => {
|
||||
expect(typeof new FlyoutCreateDrilldownAction(actionParams).getDisplayName() === 'string').toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('icon exists', () => {
|
||||
expect(typeof new FlyoutCreateDrilldownAction(actionParams).getIconType() === 'string').toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
const drilldownAction = new FlyoutCreateDrilldownAction(actionParams);
|
||||
|
||||
function checkCompatibility(params: {
|
||||
isEdit: boolean;
|
||||
withUiActions: boolean;
|
||||
isValueClickTriggerSupported: boolean;
|
||||
}): Promise<boolean> {
|
||||
return drilldownAction.isCompatible({
|
||||
embeddable: new MockEmbeddable(
|
||||
{ id: '', viewMode: params.isEdit ? ViewMode.EDIT : ViewMode.VIEW },
|
||||
{
|
||||
supportedTriggers: (params.isValueClickTriggerSupported
|
||||
? ['VALUE_CLICK_TRIGGER']
|
||||
: []) as Array<keyof TriggerContextMapping>,
|
||||
uiActions: params.withUiActions ? uiActions : undefined, // dynamic actions support
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
test("compatible if dynamicUiActions enabled, 'VALUE_CLICK_TRIGGER' is supported, in edit mode", async () => {
|
||||
expect(
|
||||
await checkCompatibility({
|
||||
withUiActions: true,
|
||||
isEdit: true,
|
||||
isValueClickTriggerSupported: true,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('not compatible if dynamicUiActions disabled', async () => {
|
||||
expect(
|
||||
await checkCompatibility({
|
||||
withUiActions: false,
|
||||
isEdit: true,
|
||||
isValueClickTriggerSupported: true,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("not compatible if 'VALUE_CLICK_TRIGGER' is not supported", async () => {
|
||||
expect(
|
||||
await checkCompatibility({
|
||||
withUiActions: true,
|
||||
isEdit: true,
|
||||
isValueClickTriggerSupported: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('not compatible if in view mode', async () => {
|
||||
expect(
|
||||
await checkCompatibility({
|
||||
withUiActions: true,
|
||||
isEdit: false,
|
||||
isValueClickTriggerSupported: true,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
const drilldownAction = new FlyoutCreateDrilldownAction(actionParams);
|
||||
test('throws error if no dynamicUiActions', async () => {
|
||||
await expect(
|
||||
drilldownAction.execute({
|
||||
embeddable: new MockEmbeddable({ id: '' }, {}),
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't execute FlyoutCreateDrilldownAction without dynamicActionsManager"`
|
||||
);
|
||||
});
|
||||
|
||||
test('should open flyout', async () => {
|
||||
const spy = jest.spyOn(overlays, 'openFlyout');
|
||||
await drilldownAction.execute({
|
||||
embeddable: new MockEmbeddable({ id: '' }, { uiActions }),
|
||||
});
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { DrilldownsStart } from '../../../../../../drilldowns/public';
|
||||
import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
|
||||
export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN';
|
||||
|
||||
export interface OpenFlyoutAddDrilldownParams {
|
||||
overlays: () => Promise<CoreStart['overlays']>;
|
||||
drilldowns: () => Promise<DrilldownsStart>;
|
||||
}
|
||||
|
||||
export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLYOUT_ADD_DRILLDOWN> {
|
||||
public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN;
|
||||
public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN;
|
||||
public order = 12;
|
||||
|
||||
constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('xpack.dashboard.FlyoutCreateDrilldownAction.displayName', {
|
||||
defaultMessage: 'Create drilldown',
|
||||
});
|
||||
}
|
||||
|
||||
public getIconType() {
|
||||
return 'plusInCircle';
|
||||
}
|
||||
|
||||
private isEmbeddableCompatible(context: EmbeddableContext) {
|
||||
if (!context.embeddable.dynamicActions) return false;
|
||||
const supportedTriggers = context.embeddable.supportedTriggers();
|
||||
if (!supportedTriggers || !supportedTriggers.length) return false;
|
||||
return supportedTriggers.indexOf('VALUE_CLICK_TRIGGER') > -1;
|
||||
}
|
||||
|
||||
public async isCompatible(context: EmbeddableContext) {
|
||||
const isEditMode = context.embeddable.getInput().viewMode === 'edit';
|
||||
return isEditMode && this.isEmbeddableCompatible(context);
|
||||
}
|
||||
|
||||
public async execute(context: EmbeddableContext) {
|
||||
const overlays = await this.params.overlays();
|
||||
const drilldowns = await this.params.drilldowns();
|
||||
const dynamicActionManager = context.embeddable.dynamicActions;
|
||||
|
||||
if (!dynamicActionManager) {
|
||||
throw new Error(`Can't execute FlyoutCreateDrilldownAction without dynamicActionsManager`);
|
||||
}
|
||||
|
||||
const handle = overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<drilldowns.FlyoutManageDrilldowns
|
||||
onClose={() => handle.close()}
|
||||
placeContext={context}
|
||||
viewMode={'create'}
|
||||
dynamicActionManager={dynamicActionManager}
|
||||
/>
|
||||
),
|
||||
{
|
||||
ownFocus: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
FlyoutCreateDrilldownAction,
|
||||
OpenFlyoutAddDrilldownParams,
|
||||
OPEN_FLYOUT_ADD_DRILLDOWN,
|
||||
} from './flyout_create_drilldown';
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* 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 { FlyoutEditDrilldownAction, FlyoutEditDrilldownParams } from './flyout_edit_drilldown';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import { drilldownsPluginMock } from '../../../../../../drilldowns/public/mocks';
|
||||
import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { uiActionsPluginMock } from '../../../../../../../../src/plugins/ui_actions/public/mocks';
|
||||
import { MockEmbeddable } from '../test_helpers';
|
||||
|
||||
const overlays = coreMock.createStart().overlays;
|
||||
const drilldowns = drilldownsPluginMock.createStartContract();
|
||||
const uiActions = uiActionsPluginMock.createStartContract();
|
||||
|
||||
const actionParams: FlyoutEditDrilldownParams = {
|
||||
drilldowns: () => Promise.resolve(drilldowns),
|
||||
overlays: () => Promise.resolve(overlays),
|
||||
};
|
||||
|
||||
test('should create', () => {
|
||||
expect(() => new FlyoutEditDrilldownAction(actionParams)).not.toThrow();
|
||||
});
|
||||
|
||||
test('title is a string', () => {
|
||||
expect(typeof new FlyoutEditDrilldownAction(actionParams).getDisplayName() === 'string').toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('icon exists', () => {
|
||||
expect(typeof new FlyoutEditDrilldownAction(actionParams).getIconType() === 'string').toBe(true);
|
||||
});
|
||||
|
||||
test('MenuItem exists', () => {
|
||||
expect(new FlyoutEditDrilldownAction(actionParams).MenuItem).toBeDefined();
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
const drilldownAction = new FlyoutEditDrilldownAction(actionParams);
|
||||
|
||||
function checkCompatibility(params: {
|
||||
isEdit: boolean;
|
||||
withUiActions: boolean;
|
||||
}): Promise<boolean> {
|
||||
return drilldownAction.isCompatible({
|
||||
embeddable: new MockEmbeddable(
|
||||
{
|
||||
id: '',
|
||||
viewMode: params.isEdit ? ViewMode.EDIT : ViewMode.VIEW,
|
||||
},
|
||||
{
|
||||
uiActions: params.withUiActions ? uiActions : undefined, // dynamic actions support
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: need proper DynamicActionsMock and ActionFactory mock
|
||||
test.todo('compatible if dynamicUiActions enabled, in edit view, and have at least 1 drilldown');
|
||||
|
||||
test('not compatible if dynamicUiActions disabled', async () => {
|
||||
expect(
|
||||
await checkCompatibility({
|
||||
withUiActions: false,
|
||||
isEdit: true,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('not compatible if no drilldowns', async () => {
|
||||
expect(
|
||||
await checkCompatibility({
|
||||
withUiActions: true,
|
||||
isEdit: true,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
const drilldownAction = new FlyoutEditDrilldownAction(actionParams);
|
||||
test('throws error if no dynamicUiActions', async () => {
|
||||
await expect(
|
||||
drilldownAction.execute({
|
||||
embeddable: new MockEmbeddable({ id: '' }, {}),
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't execute FlyoutEditDrilldownAction without dynamicActionsManager"`
|
||||
);
|
||||
});
|
||||
|
||||
test('should open flyout', async () => {
|
||||
const spy = jest.spyOn(overlays, 'openFlyout');
|
||||
await drilldownAction.execute({
|
||||
embeddable: new MockEmbeddable({ id: '' }, { uiActions }),
|
||||
});
|
||||
expect(spy).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* 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 { CoreStart } from 'src/core/public';
|
||||
import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
reactToUiComponent,
|
||||
toMountPoint,
|
||||
} from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { DrilldownsStart } from '../../../../../../drilldowns/public';
|
||||
import { txtDisplayName } from './i18n';
|
||||
import { MenuItem } from './menu_item';
|
||||
|
||||
export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN';
|
||||
|
||||
export interface FlyoutEditDrilldownParams {
|
||||
overlays: () => Promise<CoreStart['overlays']>;
|
||||
drilldowns: () => Promise<DrilldownsStart>;
|
||||
}
|
||||
|
||||
export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOUT_EDIT_DRILLDOWN> {
|
||||
public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN;
|
||||
public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN;
|
||||
public order = 10;
|
||||
|
||||
constructor(protected readonly params: FlyoutEditDrilldownParams) {}
|
||||
|
||||
public getDisplayName() {
|
||||
return txtDisplayName;
|
||||
}
|
||||
|
||||
public getIconType() {
|
||||
return 'list';
|
||||
}
|
||||
|
||||
MenuItem = reactToUiComponent(MenuItem);
|
||||
|
||||
public async isCompatible({ embeddable }: EmbeddableContext) {
|
||||
if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false;
|
||||
if (!embeddable.dynamicActions) return false;
|
||||
return embeddable.dynamicActions.state.get().events.length > 0;
|
||||
}
|
||||
|
||||
public async execute(context: EmbeddableContext) {
|
||||
const overlays = await this.params.overlays();
|
||||
const drilldowns = await this.params.drilldowns();
|
||||
const dynamicActionManager = context.embeddable.dynamicActions;
|
||||
if (!dynamicActionManager) {
|
||||
throw new Error(`Can't execute FlyoutEditDrilldownAction without dynamicActionsManager`);
|
||||
}
|
||||
|
||||
const handle = overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<drilldowns.FlyoutManageDrilldowns
|
||||
onClose={() => handle.close()}
|
||||
placeContext={context}
|
||||
viewMode={'manage'}
|
||||
dynamicActionManager={dynamicActionManager}
|
||||
/>
|
||||
),
|
||||
{
|
||||
ownFocus: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
FlyoutEditDrilldownAction,
|
||||
FlyoutEditDrilldownParams,
|
||||
OPEN_FLYOUT_EDIT_DRILLDOWN,
|
||||
} from './flyout_edit_drilldown';
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* 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 { render, cleanup, act } from '@testing-library/react/pure';
|
||||
import { MenuItem } from './menu_item';
|
||||
import { createStateContainer } from '../../../../../../../../src/plugins/kibana_utils/common';
|
||||
import { DynamicActionManager } from '../../../../../../../../src/plugins/ui_actions/public';
|
||||
import { IEmbeddable } from '../../../../../../../../src/plugins/embeddable/public/lib/embeddables';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
test('<MenuItem/>', () => {
|
||||
const state = createStateContainer<{ events: object[] }>({ events: [] });
|
||||
const { getByText, queryByText } = render(
|
||||
<MenuItem
|
||||
context={{
|
||||
embeddable: ({
|
||||
dynamicActions: ({ state } as unknown) as DynamicActionManager,
|
||||
} as unknown) as IEmbeddable,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByText(/manage drilldowns/i)).toBeInTheDocument();
|
||||
expect(queryByText('0')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
state.set({ events: [{}] });
|
||||
});
|
||||
|
||||
expect(queryByText('1')).toBeInTheDocument();
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* 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, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { txtDisplayName } from './i18n';
|
||||
import { useContainerState } from '../../../../../../../../src/plugins/kibana_utils/common';
|
||||
|
||||
export const MenuItem: React.FC<{ context: EmbeddableContext }> = ({ context }) => {
|
||||
if (!context.embeddable.dynamicActions)
|
||||
throw new Error('Flyout edit drillldown context menu item requires `dynamicActions`');
|
||||
|
||||
const { events } = useContainerState(context.embeddable.dynamicActions.state);
|
||||
const count = events.length;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
<EuiFlexItem grow={true}>{txtDisplayName}</EuiFlexItem>
|
||||
{count > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiNotificationBadge>{count}</EuiNotificationBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* 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 { Embeddable, EmbeddableInput } from '../../../../../../../src/plugins/embeddable/public/';
|
||||
import {
|
||||
TriggerContextMapping,
|
||||
UiActionsStart,
|
||||
} from '../../../../../../../src/plugins/ui_actions/public';
|
||||
|
||||
export class MockEmbeddable extends Embeddable {
|
||||
public readonly type = 'mock';
|
||||
private readonly triggers: Array<keyof TriggerContextMapping> = [];
|
||||
constructor(
|
||||
initialInput: EmbeddableInput,
|
||||
params: { uiActions?: UiActionsStart; supportedTriggers?: Array<keyof TriggerContextMapping> }
|
||||
) {
|
||||
super(initialInput, {}, undefined, params);
|
||||
this.triggers = params.supportedTriggers ?? [];
|
||||
}
|
||||
public render(node: HTMLElement) {}
|
||||
public reload() {}
|
||||
public supportedTriggers(): Array<keyof TriggerContextMapping> {
|
||||
return this.triggers;
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* 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 { CoreSetup } from 'src/core/public';
|
||||
import { SetupDependencies } from '../../plugin';
|
||||
import {
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
EmbeddableContext,
|
||||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import {
|
||||
FlyoutCreateDrilldownAction,
|
||||
FlyoutEditDrilldownAction,
|
||||
OPEN_FLYOUT_ADD_DRILLDOWN,
|
||||
OPEN_FLYOUT_EDIT_DRILLDOWN,
|
||||
} from './actions';
|
||||
import { DrilldownsStart } from '../../../../drilldowns/public';
|
||||
import { DashboardToDashboardDrilldown } from './dashboard_to_dashboard_drilldown';
|
||||
|
||||
declare module '../../../../../../src/plugins/ui_actions/public' {
|
||||
export interface ActionContextMapping {
|
||||
[OPEN_FLYOUT_ADD_DRILLDOWN]: EmbeddableContext;
|
||||
[OPEN_FLYOUT_EDIT_DRILLDOWN]: EmbeddableContext;
|
||||
}
|
||||
}
|
||||
|
||||
interface BootstrapParams {
|
||||
enableDrilldowns: boolean;
|
||||
}
|
||||
|
||||
export class DashboardDrilldownsService {
|
||||
bootstrap(
|
||||
core: CoreSetup<{ drilldowns: DrilldownsStart }>,
|
||||
plugins: SetupDependencies,
|
||||
{ enableDrilldowns }: BootstrapParams
|
||||
) {
|
||||
if (enableDrilldowns) {
|
||||
this.setupDrilldowns(core, plugins);
|
||||
}
|
||||
}
|
||||
|
||||
setupDrilldowns(core: CoreSetup<{ drilldowns: DrilldownsStart }>, plugins: SetupDependencies) {
|
||||
const overlays = async () => (await core.getStartServices())[0].overlays;
|
||||
const drilldowns = async () => (await core.getStartServices())[1].drilldowns;
|
||||
const savedObjects = async () => (await core.getStartServices())[0].savedObjects.client;
|
||||
|
||||
const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays, drilldowns });
|
||||
plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown);
|
||||
|
||||
const actionFlyoutEditDrilldown = new FlyoutEditDrilldownAction({ overlays, drilldowns });
|
||||
plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown);
|
||||
|
||||
const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({
|
||||
savedObjects,
|
||||
});
|
||||
plugins.drilldowns.registerDrilldown(dashboardToDashboardDrilldown);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
test.todo('displays all dashboard in a list');
|
||||
test.todo('does not display dashboard on which drilldown is being created');
|
||||
test.todo('updates config object correctly');
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
import { CollectConfigProps } from './types';
|
||||
import { DashboardDrilldownConfig } from '../../../components/dashboard_drilldown_config';
|
||||
import { Params } from './drilldown';
|
||||
|
||||
export interface CollectConfigContainerProps extends CollectConfigProps {
|
||||
params: Params;
|
||||
}
|
||||
|
||||
export const CollectConfigContainer: React.FC<CollectConfigContainerProps> = ({
|
||||
config,
|
||||
onConfig,
|
||||
params: { savedObjects },
|
||||
}) => {
|
||||
const [dashboards] = useState([
|
||||
{ id: 'dashboard1', title: 'Dashboard 1' },
|
||||
{ id: 'dashboard2', title: 'Dashboard 2' },
|
||||
{ id: 'dashboard3', title: 'Dashboard 3' },
|
||||
{ id: 'dashboard4', title: 'Dashboard 4' },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Load dashboards...
|
||||
}, [savedObjects]);
|
||||
|
||||
return (
|
||||
<DashboardDrilldownConfig
|
||||
activeDashboardId={config.dashboardId}
|
||||
dashboards={dashboards}
|
||||
currentFilters={config.useCurrentFilters}
|
||||
keepRange={config.useCurrentDateRange}
|
||||
onDashboardSelect={dashboardId => {
|
||||
onConfig({ ...config, dashboardId });
|
||||
}}
|
||||
onCurrentFiltersToggle={() =>
|
||||
onConfig({
|
||||
...config,
|
||||
useCurrentFilters: !config.useCurrentFilters,
|
||||
})
|
||||
}
|
||||
onKeepRangeToggle={() =>
|
||||
onConfig({
|
||||
...config,
|
||||
useCurrentDateRange: !config.useCurrentDateRange,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue