From 788745e810487a48d905e813c711891ce9e58de7 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 12 Feb 2024 10:40:34 -0700 Subject: [PATCH] decouple dashboard drilldown from Embeddables framework (#176188) part of https://github.com/elastic/kibana/issues/175138 PR decouples FlyoutCreateDrilldownAction, FlyoutEditDrilldownAction, and EmbeddableToDashboardDrilldown from Embeddable framework. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../interfaces/presentation_container.ts | 16 +- .../presentation_containers/mocks.ts | 2 + .../presentation_publishing/index.ts | 4 + .../interfaces/has_supported_triggers.ts | 15 ++ .../locator/get_dashboard_locator_params.ts | 33 ++- .../create/create_dashboard.test.ts | 2 + .../embeddable/create/create_dashboard.ts | 45 +++- .../public/lib/triggers/triggers.ts | 3 + .../dashboard_link_component.tsx | 8 +- src/plugins/links/tsconfig.json | 3 +- .../plugins/dashboard_enhanced/kibana.jsonc | 3 +- .../drilldowns/actions/drilldown_shared.ts | 43 ++-- .../flyout_create_drilldown.test.tsx | 220 +++++++++--------- .../flyout_create_drilldown.tsx | 109 ++++----- .../flyout_edit_drilldown.test.tsx | 200 ++++++++-------- .../flyout_edit_drilldown.tsx | 79 +++---- .../drilldowns/actions/test_helpers.ts | 49 ---- ...embeddable_to_dashboard_drilldown.test.tsx | 22 +- .../embeddable_to_dashboard_drilldown.tsx | 8 +- .../plugins/dashboard_enhanced/tsconfig.json | 4 +- .../public/actions/drilldown_grouping.ts | 5 +- .../interfaces/has_dynamic_actions.ts | 20 ++ .../embeddable_enhanced/public/index.ts | 4 + .../embeddable_enhanced/public/types.ts | 3 + 24 files changed, 463 insertions(+), 437 deletions(-) create mode 100644 packages/presentation/presentation_publishing/interfaces/has_supported_triggers.ts delete mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/embeddables/interfaces/has_dynamic_actions.ts diff --git a/packages/presentation/presentation_containers/interfaces/presentation_container.ts b/packages/presentation/presentation_containers/interfaces/presentation_container.ts index a92c5af7cbd0a..b0c7c3cdb64da 100644 --- a/packages/presentation/presentation_containers/interfaces/presentation_container.ts +++ b/packages/presentation/presentation_containers/interfaces/presentation_container.ts @@ -27,16 +27,18 @@ export type PresentationContainer = Partial & removePanel: (panelId: string) => void; canRemovePanels?: () => boolean; replacePanel: (idToRemove: string, newPanel: PanelPackage) => Promise; + getChildIds: () => string[]; + getChild: (childId: string) => unknown; }; -export const apiIsPresentationContainer = ( - unknownApi: unknown | null -): unknownApi is PresentationContainer => { +export const apiIsPresentationContainer = (api: unknown | null): api is PresentationContainer => { return Boolean( - (unknownApi as PresentationContainer)?.removePanel !== undefined && - (unknownApi as PresentationContainer)?.registerPanelApi !== undefined && - (unknownApi as PresentationContainer)?.replacePanel !== undefined && - (unknownApi as PresentationContainer)?.addNewPanel !== undefined + typeof (api as PresentationContainer)?.removePanel === 'function' && + typeof (api as PresentationContainer)?.registerPanelApi === 'function' && + typeof (api as PresentationContainer)?.replacePanel === 'function' && + typeof (api as PresentationContainer)?.addNewPanel === 'function' && + typeof (api as PresentationContainer)?.getChildIds === 'function' && + typeof (api as PresentationContainer)?.getChild === 'function' ); }; diff --git a/packages/presentation/presentation_containers/mocks.ts b/packages/presentation/presentation_containers/mocks.ts index 6d4610075c9d5..ac84c8cc5fd4b 100644 --- a/packages/presentation/presentation_containers/mocks.ts +++ b/packages/presentation/presentation_containers/mocks.ts @@ -17,5 +17,7 @@ export const getMockPresentationContainer = (): PresentationContainer => { registerPanelApi: jest.fn(), lastSavedState: new Subject(), getLastSavedStateForChild: jest.fn(), + getChildIds: jest.fn(), + getChild: jest.fn(), }; }; diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts index 80d2c5870efbe..10c2f644b16b1 100644 --- a/packages/presentation/presentation_publishing/index.ts +++ b/packages/presentation/presentation_publishing/index.ts @@ -89,6 +89,10 @@ export { } from './interfaces/publishes_saved_object_id'; export { apiHasUniqueId, type HasUniqueId } from './interfaces/has_uuid'; export { apiHasDisableTriggers, type HasDisableTriggers } from './interfaces/has_disable_triggers'; +export { + apiHasSupportedTriggers, + type HasSupportedTriggers, +} from './interfaces/has_supported_triggers'; export { apiPublishesViewMode, apiPublishesWritableViewMode, diff --git a/packages/presentation/presentation_publishing/interfaces/has_supported_triggers.ts b/packages/presentation/presentation_publishing/interfaces/has_supported_triggers.ts new file mode 100644 index 0000000000000..d3a04522abb87 --- /dev/null +++ b/packages/presentation/presentation_publishing/interfaces/has_supported_triggers.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface HasSupportedTriggers { + supportedTriggers: () => string[]; +} + +export const apiHasSupportedTriggers = (api: unknown | null): api is HasSupportedTriggers => { + return Boolean(api && typeof (api as HasSupportedTriggers).supportedTriggers === 'function'); +}; diff --git a/src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts b/src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts index ff078be8f62f1..0843029ac3fc0 100644 --- a/src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts +++ b/src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts @@ -6,41 +6,34 @@ * Side Public License, v 1. */ -import { isQuery, isTimeRange } from '@kbn/data-plugin/common'; -import { Filter, isFilterPinned, Query, TimeRange } from '@kbn/es-query'; -import { EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public'; -import { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public'; -import { DashboardLocatorParams } from '../../dashboard_container'; - -interface EmbeddableQueryInput extends EmbeddableInput { - query?: Query; - filters?: Filter[]; - timeRange?: TimeRange; -} +import { isFilterPinned, type Query } from '@kbn/es-query'; +import type { HasParentApi, PublishesLocalUnifiedSearch } from '@kbn/presentation-publishing'; +import type { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public'; +import type { DashboardLocatorParams } from '../../dashboard_container'; export const getDashboardLocatorParamsFromEmbeddable = ( - source: IEmbeddable, + api: Partial>>, options: DashboardDrilldownOptions ): Partial => { const params: DashboardLocatorParams = {}; - const input = source.getInput(); - if (isQuery(input.query) && options.useCurrentFilters) { - params.query = input.query; + const query = api.parentApi?.localQuery?.value; + if (query && options.useCurrentFilters) { + params.query = query as Query; } // if useCurrentDashboardDataRange is enabled, then preserve current time range // if undefined is passed, then destination dashboard will figure out time range itself // for brush event this time range would be overwritten - if (isTimeRange(input.timeRange) && options.useCurrentDateRange) { - params.timeRange = input.timeRange; + const timeRange = api.localTimeRange?.value ?? api.parentApi?.localTimeRange?.value; + if (timeRange && options.useCurrentDateRange) { + params.timeRange = timeRange; } // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned, unpinned, and from controls) // otherwise preserve only pinned - params.filters = options.useCurrentFilters - ? input.filters - : input.filters?.filter((f) => isFilterPinned(f)); + const filters = api.parentApi?.localFilters?.value ?? []; + params.filters = options.useCurrentFilters ? filters : filters?.filter((f) => isFilterPinned(f)); return params; }; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts index 5ae88b7c3bcd3..12eebe31da173 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts @@ -428,7 +428,9 @@ test('creates a control group from the control group factory and waits for it to untilInitialized: jest.fn(), getInput: jest.fn().mockReturnValue({}), getInput$: jest.fn().mockReturnValue(new Observable()), + getOutput: jest.fn().mockReturnValue({}), getOutput$: jest.fn().mockReturnValue(new Observable()), + onFiltersPublished$: new Observable(), unsavedChanges: new BehaviorSubject(undefined), } as unknown as ControlGroupContainer; const mockControlGroupFactory = { diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index 93b849a122ce0..d7d34fa42f078 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -23,11 +23,13 @@ import { reactEmbeddableRegistryHasKey, ViewMode, } from '@kbn/embeddable-plugin/public'; -import { TimeRange } from '@kbn/es-query'; +import { compareFilters, Filter, TimeRange } from '@kbn/es-query'; import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public'; import { cloneDeep, identity, omit, pickBy } from 'lodash'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, combineLatest, Subject } from 'rxjs'; +import { map, distinctUntilChanged, startWith } from 'rxjs/operators'; import { v4 } from 'uuid'; +import { combineDashboardFiltersWithControlGroupFilters } from './controls/dashboard_control_group_integration'; import { DashboardContainerInput, DashboardPanelState } from '../../../../common'; import { DEFAULT_DASHBOARD_INPUT, @@ -462,5 +464,44 @@ export const initializeDashboard = async ({ setTimeout(() => dashboard.dispatch.setAnimatePanelTransforms(true), 500) ); + // -------------------------------------------------------------------------------------- + // Set parentApi.localFilters to include dashboardContainer filters and control group filters + // -------------------------------------------------------------------------------------- + untilDashboardReady().then((dashboardContainer) => { + if (!dashboardContainer.controlGroup) { + return; + } + + function getCombinedFilters() { + return combineDashboardFiltersWithControlGroupFilters( + dashboardContainer.getInput().filters ?? [], + dashboardContainer.controlGroup! + ); + } + + const localFilters = new BehaviorSubject(getCombinedFilters()); + dashboardContainer.localFilters = localFilters; + + const inputFilters$ = dashboardContainer.getInput$().pipe( + startWith(dashboardContainer.getInput()), + map((input) => input.filters), + distinctUntilChanged((previous, current) => { + return compareFilters(previous ?? [], current ?? []); + }) + ); + + // Can not use onFiltersPublished$ directly since it does not have an intial value and + // combineLatest will not emit until each observable emits at least one value + const controlGroupFilters$ = dashboardContainer.controlGroup.onFiltersPublished$.pipe( + startWith(dashboardContainer.controlGroup.getOutput().filters) + ); + + dashboardContainer.integrationSubscriptions.add( + combineLatest([inputFilters$, controlGroupFilters$]).subscribe(() => { + localFilters.next(getCombinedFilters()); + }) + ); + }); + return { input: initialDashboardInput, searchSessionId: initialSearchSessionId }; }; diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index e60e6a2146a7c..edefdb191e123 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -13,6 +13,9 @@ import { Trigger, RowClickContext } from '@kbn/ui-actions-plugin/public'; import { BooleanRelation } from '@kbn/es-query'; import { IEmbeddable } from '..'; +/** + * @deprecated use `EmbeddableApiContext` from `@kbn/presentation-publishing` + */ export interface EmbeddableContext { embeddable: T; } diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx index b6bbe8fa5f68b..90bf4a03002b7 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx @@ -22,6 +22,7 @@ import { DashboardDrilldownOptions, DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS, } from '@kbn/presentation-util-plugin/public'; +import type { HasParentApi, PublishesLocalUnifiedSearch } from '@kbn/presentation-publishing'; import { DASHBOARD_LINK_TYPE, @@ -115,7 +116,12 @@ export const DashboardLinkComponent = ({ const params: DashboardLocatorParams = { dashboardId: link.destination, - ...getDashboardLocatorParamsFromEmbeddable(linksEmbeddable, linkOptions), + ...getDashboardLocatorParamsFromEmbeddable( + linksEmbeddable as Partial< + PublishesLocalUnifiedSearch & HasParentApi> + >, + linkOptions + ), }; const locator = dashboardContainer.locator; diff --git a/src/plugins/links/tsconfig.json b/src/plugins/links/tsconfig.json index 0b4430d5acc59..3431fdfd78a65 100644 --- a/src/plugins/links/tsconfig.json +++ b/src/plugins/links/tsconfig.json @@ -31,7 +31,8 @@ "@kbn/usage-collection-plugin", "@kbn/visualizations-plugin", "@kbn/core-mount-utils-browser", - "@kbn/presentation-containers" + "@kbn/presentation-containers", + "@kbn/presentation-publishing" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/dashboard_enhanced/kibana.jsonc b/x-pack/plugins/dashboard_enhanced/kibana.jsonc index 88bb64bb00503..c37509d0a669a 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.jsonc +++ b/x-pack/plugins/dashboard_enhanced/kibana.jsonc @@ -21,7 +21,8 @@ "kibanaReact", "kibanaUtils", "imageEmbeddable", - "presentationUtil" + "presentationUtil", + "uiActions" ] } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts index b9cfe79e19fa9..65b47346e50aa 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts @@ -6,13 +6,17 @@ */ import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; +import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public'; import { - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, - IEmbeddable, - Container as EmbeddableContainer, -} from '@kbn/embeddable-plugin/public'; -import { isEnhancedEmbeddable } from '@kbn/embeddable-enhanced-plugin/public'; + apiIsPresentationContainer, + type PresentationContainer, +} from '@kbn/presentation-containers'; +import { + PublishesPanelTitle, + type HasUniqueId, + type HasParentApi, +} from '@kbn/presentation-publishing'; +import { apiHasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public'; import { UiActionsEnhancedDrilldownTemplate as DrilldownTemplate } from '@kbn/ui-actions-enhanced-plugin/public'; /** @@ -36,31 +40,22 @@ export function ensureNestedTriggers(triggers: string[]): string[] { return triggers; } -const isEmbeddableContainer = (x: unknown): x is EmbeddableContainer => - x instanceof EmbeddableContainer; - /** * Given a dashboard panel embeddable, it will find the parent (dashboard * container embeddable), then iterate through all the dashboard panels and * generate DrilldownTemplate for each existing drilldown. */ export const createDrilldownTemplatesFromSiblings = ( - embeddable: IEmbeddable + embeddable: Partial & HasParentApi> ): DrilldownTemplate[] => { - const templates: DrilldownTemplate[] = []; - const embeddableId = embeddable.id; - - const container = embeddable.getRoot(); + const parentApi = embeddable.parentApi; + if (!apiIsPresentationContainer(parentApi)) return []; - if (!container) return templates; - if (!isEmbeddableContainer(container)) return templates; - - const childrenIds = (container as EmbeddableContainer).getChildIds(); - - for (const childId of childrenIds) { - const child = (container as EmbeddableContainer).getChild(childId); - if (child.id === embeddableId) continue; - if (!isEnhancedEmbeddable(child)) continue; + const templates: DrilldownTemplate[] = []; + for (const childId of parentApi.getChildIds()) { + const child = parentApi.getChild(childId) as Partial; + if (childId === embeddable.uuid) continue; + if (!apiHasDynamicActions(child)) continue; const events = child.enhancements.dynamicActions.state.get().events; for (const event of events) { @@ -68,7 +63,7 @@ export const createDrilldownTemplatesFromSiblings = ( id: event.eventId, name: event.action.name, icon: 'dashboardApp', - description: child.getTitle() || child.id, + description: child.panelTitle?.value ?? child.uuid ?? '', config: event.action.config, factoryId: event.action.factoryId, triggers: event.triggers, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index 165f80cdf51e8..50fe2e570b9a6 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -5,164 +5,152 @@ * 2.0. */ -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { + UiActionsEnhancedMemoryActionStorage as MemoryActionStorage, + UiActionsEnhancedDynamicActionManager as DynamicActionManager, +} from '@kbn/ui-actions-enhanced-plugin/public'; +import type { ViewMode } from '@kbn/presentation-publishing'; import { FlyoutCreateDrilldownAction, OpenFlyoutAddDrilldownParams, } from './flyout_create_drilldown'; import { coreMock } from '@kbn/core/public/mocks'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; import { uiActionsEnhancedPluginMock } from '@kbn/ui-actions-enhanced-plugin/public/mocks'; import { UiActionsEnhancedActionFactory } from '@kbn/ui-actions-enhanced-plugin/public'; -const overlays = coreMock.createStart().overlays; -const uiActionsEnhanced = uiActionsEnhancedPluginMock.createStartContract(); - -const actionParams: OpenFlyoutAddDrilldownParams = { - start: () => ({ - core: { - overlays, - application: { - currentAppId$: new Subject(), +function createAction( + allPossibleTriggers = ['VALUE_CLICK_TRIGGER'], + overlays = coreMock.createStart().overlays +) { + const uiActionsEnhanced = uiActionsEnhancedPluginMock.createStartContract(); + const params: OpenFlyoutAddDrilldownParams = { + start: () => ({ + core: { + overlays, + application: { + currentAppId$: new Subject(), + }, + theme: { + theme$: new Subject(), + }, + } as any, + plugins: { + uiActionsEnhanced, }, - theme: { - theme$: new Subject(), - }, - } as any, - plugins: { - uiActionsEnhanced, - }, - self: {}, - }), -}; + self: {}, + }), + }; -test('should create', () => { - expect(() => new FlyoutCreateDrilldownAction(actionParams)).not.toThrow(); -}); + uiActionsEnhanced.getActionFactories.mockImplementation(() => [ + { + supportedTriggers: () => allPossibleTriggers, + isCompatibleLicense: () => true, + } as unknown as UiActionsEnhancedActionFactory, + ]); + return new FlyoutCreateDrilldownAction(params); +} + +const compatibleEmbeddableApi = { + enhancements: { + dynamicActions: new DynamicActionManager({ + storage: new MemoryActionStorage(), + isCompatible: async () => true, + uiActions: uiActionsEnhancedPluginMock.createStartContract(), + }), + }, + parentApi: { + type: 'dashboard', + }, + supportedTriggers: () => { + return ['VALUE_CLICK_TRIGGER']; + }, + viewMode: new BehaviorSubject('edit'), +}; test('title is a string', () => { - expect(typeof new FlyoutCreateDrilldownAction(actionParams).getDisplayName() === 'string').toBe( - true - ); + expect(typeof createAction().getDisplayName() === 'string').toBe(true); }); test('icon exists', () => { - expect(typeof new FlyoutCreateDrilldownAction(actionParams).getIconType() === 'string').toBe( - true - ); + expect(typeof createAction().getIconType() === 'string').toBe(true); }); -interface CompatibilityParams { - isEdit?: boolean; - isValueClickTriggerSupported?: boolean; - isEmbeddableEnhanced?: boolean; - rootType?: string; - actionFactoriesTriggers?: string[]; -} - describe('isCompatible', () => { - const drilldownAction = new FlyoutCreateDrilldownAction(actionParams); - - async function assertCompatibility( - { - isEdit = true, - isValueClickTriggerSupported = true, - isEmbeddableEnhanced = true, - rootType = 'dashboard', - actionFactoriesTriggers = ['VALUE_CLICK_TRIGGER'], - }: CompatibilityParams, - expectedResult: boolean = true - ): Promise { - uiActionsEnhanced.getActionFactories.mockImplementation(() => [ - { - supportedTriggers: () => actionFactoriesTriggers, - isCompatibleLicense: () => true, - } as unknown as UiActionsEnhancedActionFactory, - ]); - - let embeddable = new MockEmbeddable( - { id: '', viewMode: isEdit ? ViewMode.EDIT : ViewMode.VIEW }, - { - supportedTriggers: isValueClickTriggerSupported ? ['VALUE_CLICK_TRIGGER'] : [], - } - ); - - embeddable.rootType = rootType; - - if (isEmbeddableEnhanced) { - embeddable = enhanceEmbeddable(embeddable); - } - - const result = await drilldownAction.isCompatible({ - embeddable, - }); - - expect(result).toBe(expectedResult); - } - - const assertNonCompatibility = (params: CompatibilityParams) => - assertCompatibility(params, false); - test("compatible if dynamicUiActions enabled, 'VALUE_CLICK_TRIGGER' is supported, in edit mode", async () => { - await assertCompatibility({}); + const action = createAction(); + expect(await action.isCompatible({ embeddable: compatibleEmbeddableApi })).toBe(true); }); test('not compatible if embeddable is not enhanced', async () => { - await assertNonCompatibility({ - isEmbeddableEnhanced: false, - }); + const action = createAction(); + const embeddableApi = { + ...compatibleEmbeddableApi, + enhancements: undefined, + }; + expect(await action.isCompatible({ embeddable: embeddableApi })).toBe(false); }); test("not compatible if 'VALUE_CLICK_TRIGGER' is not supported", async () => { - await assertNonCompatibility({ - isValueClickTriggerSupported: false, - }); + const action = createAction(); + const embeddableApi = { + ...compatibleEmbeddableApi, + supportedTriggers: () => { + return []; + }, + }; + expect(await action.isCompatible({ embeddable: embeddableApi })).toBe(false); }); test('not compatible if in view mode', async () => { - await assertNonCompatibility({ - isEdit: false, - }); + const action = createAction(); + const embeddableApi = { + ...compatibleEmbeddableApi, + viewMode: new BehaviorSubject('view'), + }; + expect(await action.isCompatible({ embeddable: embeddableApi })).toBe(false); }); - test('not compatible if root embeddable is not "dashboard"', async () => { - await assertNonCompatibility({ - rootType: 'visualization', - }); + test('not compatible if parent embeddable is not "dashboard"', async () => { + const action = createAction(); + const embeddableApi = { + ...compatibleEmbeddableApi, + parentApi: { + type: 'visualization', + }, + }; + expect(await action.isCompatible({ embeddable: embeddableApi })).toBe(false); }); test('not compatible if no triggers intersect', async () => { - await assertNonCompatibility({ - actionFactoriesTriggers: [], - }); - await assertNonCompatibility({ - actionFactoriesTriggers: ['SELECT_RANGE_TRIGGER'], - }); + expect(await createAction([]).isCompatible({ embeddable: compatibleEmbeddableApi })).toBe( + false + ); + expect( + await createAction(['SELECT_RANGE_TRIGGER']).isCompatible({ + embeddable: compatibleEmbeddableApi, + }) + ).toBe(false); }); }); describe('execute', () => { - const drilldownAction = new FlyoutCreateDrilldownAction(actionParams); - - test('throws error if no dynamicUiActions', async () => { + test('throws if no dynamicUiActions', async () => { + const action = createAction(); + const embeddableApi = { + ...compatibleEmbeddableApi, + enhancements: undefined, + }; await expect( - drilldownAction.execute({ - embeddable: new MockEmbeddable({ id: '' }, {}), - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Need embeddable to be EnhancedEmbeddable to execute FlyoutCreateDrilldownAction."` - ); + action.execute({ embeddable: embeddableApi }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Action is incompatible"`); }); test('should open flyout', async () => { + const overlays = coreMock.createStart().overlays; const spy = jest.spyOn(overlays, 'openFlyout'); - const embeddable = enhanceEmbeddable(new MockEmbeddable({ id: '' }, {})); - - await drilldownAction.execute({ - embeddable, - }); - + const action = createAction(['VALUE_CLICK_TRIGGER'], overlays); + await action.execute({ embeddable: compatibleEmbeddableApi }); expect(spy).toBeCalled(); }); }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index 5bd16597c0b54..2c6bd8074a689 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -5,20 +5,37 @@ * 2.0. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { distinctUntilChanged, filter, map, skip, take, takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; -import { Action } from '@kbn/ui-actions-plugin/public'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { CONTEXT_MENU_TRIGGER, EmbeddableContext, ViewMode } from '@kbn/embeddable-plugin/public'; import { - isEnhancedEmbeddable, + apiHasDynamicActions, embeddableEnhancedDrilldownGrouping, + type HasDynamicActions, } from '@kbn/embeddable-enhanced-plugin/public'; +import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public'; +import { + tracksOverlays, + type PresentationContainer, + type TracksOverlays, +} from '@kbn/presentation-containers'; +import { + apiCanAccessViewMode, + apiHasParentApi, + apiHasSupportedTriggers, + apiIsOfType, + getInheritedViewMode, + type CanAccessViewMode, + type EmbeddableApiContext, + type HasUniqueId, + type HasParentApi, + type HasSupportedTriggers, + type HasType, +} from '@kbn/presentation-publishing'; +import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import React from 'react'; import { StartDependencies } from '../../../../plugin'; -import { ensureNestedTriggers, createDrilldownTemplatesFromSiblings } from '../drilldown_shared'; +import { createDrilldownTemplatesFromSiblings, ensureNestedTriggers } from '../drilldown_shared'; export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN'; @@ -26,7 +43,19 @@ export interface OpenFlyoutAddDrilldownParams { start: StartServicesGetter>; } -export class FlyoutCreateDrilldownAction implements Action { +export type FlyoutCreateDrilldownActionApi = CanAccessViewMode & + HasDynamicActions & + HasParentApi> & + HasSupportedTriggers & + Partial; + +const isApiCompatible = (api: unknown | null): api is FlyoutCreateDrilldownActionApi => + apiHasDynamicActions(api) && + apiHasParentApi(api) && + apiCanAccessViewMode(api) && + apiHasSupportedTriggers(api); + +export class FlyoutCreateDrilldownAction implements Action { public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; public order = 12; @@ -44,13 +73,15 @@ export class FlyoutCreateDrilldownAction implements Action { return 'plusInCircle'; } - private isEmbeddableCompatible(context: EmbeddableContext): boolean { - if (!isEnhancedEmbeddable(context.embeddable)) return false; - if (context.embeddable.getRoot().type !== 'dashboard') return false; - const supportedTriggers = [ - CONTEXT_MENU_TRIGGER, - ...(context.embeddable.supportedTriggers() || []), - ]; + public async isCompatible({ embeddable }: EmbeddableApiContext) { + if (!isApiCompatible(embeddable)) return false; + if ( + getInheritedViewMode(embeddable) !== 'edit' || + !apiIsOfType(embeddable.parentApi, 'dashboard') + ) + return false; + + const supportedTriggers = [CONTEXT_MENU_TRIGGER, ...embeddable.supportedTriggers()]; /** * Check if there is an intersection between all registered drilldowns possible triggers that they could be attached to @@ -67,36 +98,22 @@ export class FlyoutCreateDrilldownAction implements Action { ); } - public async isCompatible(context: EmbeddableContext) { - if (!context.embeddable?.getInput) return false; - const isEditMode = context.embeddable.getInput().viewMode === 'edit'; - return isEditMode && this.isEmbeddableCompatible(context); - } - - public async execute(context: EmbeddableContext) { + public async execute({ embeddable }: EmbeddableApiContext) { + if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); const { core, plugins } = this.params.start(); - const { embeddable } = context; - - if (!isEnhancedEmbeddable(embeddable)) { - throw new Error( - 'Need embeddable to be EnhancedEmbeddable to execute FlyoutCreateDrilldownAction.' - ); - } const templates = createDrilldownTemplatesFromSiblings(embeddable); - const closed$ = new Subject(); + const overlayTracker = tracksOverlays(embeddable.parentApi) ? embeddable.parentApi : undefined; const close = () => { - closed$.next(true); + if (overlayTracker) overlayTracker.clearOverlays(); handle.close(); }; - const closeFlyout = () => { - close(); - }; const triggers = [ ...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER, ]; + const handle = core.overlays.openFlyout( toMountPoint( { { ownFocus: true, 'data-test-subj': 'createDrilldownFlyout', + onClose: () => { + close(); + }, } ); - // Close flyout on application change. - core.application.currentAppId$ - .pipe(takeUntil(closed$), skip(1), take(1)) - .subscribe(closeFlyout); - - // Close flyout on dashboard switch to "view" mode or on embeddable destroy. - embeddable - .getInput$() - .pipe( - takeUntil(closed$), - map((input) => input.viewMode), - distinctUntilChanged(), - filter((mode) => mode !== ViewMode.EDIT), - take(1) - ) - .subscribe({ next: closeFlyout, complete: closeFlyout }); + overlayTracker?.openOverlay(handle); } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx index fe0d6df5972c4..1aa64bd6e5a9e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx @@ -5,152 +5,136 @@ * 2.0. */ -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { FlyoutEditDrilldownAction, FlyoutEditDrilldownParams } from './flyout_edit_drilldown'; import { coreMock } from '@kbn/core/public/mocks'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; +import type { ViewMode } from '@kbn/presentation-publishing'; +import { + UiActionsEnhancedMemoryActionStorage as MemoryActionStorage, + UiActionsEnhancedDynamicActionManager as DynamicActionManager, +} from '@kbn/ui-actions-enhanced-plugin/public'; import { uiActionsEnhancedPluginMock } from '@kbn/ui-actions-enhanced-plugin/public/mocks'; -import { EnhancedEmbeddable } from '@kbn/embeddable-enhanced-plugin/public'; -import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; -const overlays = coreMock.createStart().overlays; -const uiActionsPlugin = uiActionsEnhancedPluginMock.createPlugin(); -const uiActions = uiActionsPlugin.doStart(); - -uiActionsPlugin.setup.registerDrilldown({ - id: 'foo', - CollectConfig: {} as any, - createConfig: () => ({}), - isConfigValid: () => true, - execute: async () => {}, - getDisplayName: () => 'test', - supportedTriggers() { +function createAction(overlays = coreMock.createStart().overlays) { + const uiActionsPlugin = uiActionsEnhancedPluginMock.createPlugin(); + const uiActions = uiActionsPlugin.doStart(); + const params: FlyoutEditDrilldownParams = { + start: () => ({ + core: { + overlays, + application: { + currentAppId$: new Subject(), + }, + theme: { + theme$: new Subject(), + }, + } as any, + plugins: { + uiActionsEnhanced: uiActions, + }, + self: {}, + }), + }; + return new FlyoutEditDrilldownAction(params); +} + +const compatibleEmbeddableApi = { + enhancements: { + dynamicActions: new DynamicActionManager({ + storage: new MemoryActionStorage(), + isCompatible: async () => true, + uiActions: uiActionsEnhancedPluginMock.createStartContract(), + }), + }, + supportedTriggers: () => { return ['VALUE_CLICK_TRIGGER']; }, -}); - -const actionParams: FlyoutEditDrilldownParams = { - start: () => ({ - core: { - overlays, - application: { - currentAppId$: new Subject(), - }, - theme: { - theme$: new Subject(), - }, - } as any, - plugins: { - uiActionsEnhanced: uiActions, - }, - self: {}, - }), + viewMode: new BehaviorSubject('edit'), }; -test('should create', () => { - expect(() => new FlyoutEditDrilldownAction(actionParams)).not.toThrow(); +beforeAll(async () => { + await compatibleEmbeddableApi.enhancements.dynamicActions.createEvent( + { + config: {}, + factoryId: 'foo', + name: '', + }, + ['VALUE_CLICK_TRIGGER'] + ); }); test('title is a string', () => { - expect(typeof new FlyoutEditDrilldownAction(actionParams).getDisplayName() === 'string').toBe( - true - ); + expect(typeof createAction().getDisplayName() === 'string').toBe(true); }); test('icon exists', () => { - expect(typeof new FlyoutEditDrilldownAction(actionParams).getIconType() === 'string').toBe(true); + expect(typeof createAction().getIconType() === 'string').toBe(true); }); test('MenuItem exists', () => { - expect(new FlyoutEditDrilldownAction(actionParams).MenuItem).toBeDefined(); + expect(createAction().MenuItem).toBeDefined(); }); describe('isCompatible', () => { - function setupIsCompatible({ - isEdit = true, - isEmbeddableEnhanced = true, - }: { - isEdit?: boolean; - isEmbeddableEnhanced?: boolean; - } = {}) { - const action = new FlyoutEditDrilldownAction(actionParams); - const input = { - id: '', - viewMode: isEdit ? ViewMode.EDIT : ViewMode.VIEW, - }; - const embeddable = new MockEmbeddable(input, {}); - const context = { - embeddable: (isEmbeddableEnhanced - ? enhanceEmbeddable(embeddable, uiActions) - : embeddable) as EnhancedEmbeddable, - }; - - return { - action, - context, - }; - } + test("compatible if dynamicUiActions enabled (with event), 'VALUE_CLICK_TRIGGER' is supported, in edit mode", async () => { + const action = createAction(); + expect(await action.isCompatible({ embeddable: compatibleEmbeddableApi })).toBe(true); + }); test('not compatible if no drilldowns', async () => { - const { action, context } = setupIsCompatible(); - expect(await action.isCompatible(context)).toBe(false); + const embeddableApi = { + ...compatibleEmbeddableApi, + enhancements: { + dynamicActions: new DynamicActionManager({ + storage: new MemoryActionStorage(), + isCompatible: async () => true, + uiActions: uiActionsEnhancedPluginMock.createStartContract(), + }), + }, + }; + const action = createAction(); + expect(await action.isCompatible({ embeddable: embeddableApi })).toBe(false); }); test('not compatible if embeddable is not enhanced', async () => { - const { action, context } = setupIsCompatible({ isEmbeddableEnhanced: false }); - expect(await action.isCompatible(context)).toBe(false); + const embeddableApi = { + ...compatibleEmbeddableApi, + enhancements: undefined, + }; + const action = createAction(); + expect(await action.isCompatible({ embeddable: embeddableApi })).toBe(false); }); - describe('when has at least one drilldown', () => { - test('is compatible in edit mode', async () => { - const { action, context } = setupIsCompatible(); - - await context.embeddable.enhancements.dynamicActions.createEvent( - { - config: {}, - factoryId: 'foo', - name: '', - }, - ['VALUE_CLICK_TRIGGER'] - ); - - expect(await action.isCompatible(context)).toBe(true); - }); - - test('not compatible in view mode', async () => { - const { action, context } = setupIsCompatible({ isEdit: false }); - - await context.embeddable.enhancements.dynamicActions.createEvent( - { - config: {}, - factoryId: 'foo', - name: '', - }, - ['VALUE_CLICK_TRIGGER'] - ); - - expect(await action.isCompatible(context)).toBe(false); - }); + test('not compatible in view mode', async () => { + const embeddableApi = { + ...compatibleEmbeddableApi, + viewMode: new BehaviorSubject('view'), + }; + const action = createAction(); + expect(await action.isCompatible({ embeddable: embeddableApi })).toBe(false); }); }); describe('execute', () => { - const drilldownAction = new FlyoutEditDrilldownAction(actionParams); - test('throws error if no dynamicUiActions', async () => { + const action = createAction(); + const embeddableApi = { + ...compatibleEmbeddableApi, + enhancements: undefined, + }; await expect( - drilldownAction.execute({ - embeddable: new MockEmbeddable({ id: '' }, {}), + action.execute({ + embeddable: embeddableApi, }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Need embeddable to be EnhancedEmbeddable to execute FlyoutEditDrilldownAction."` - ); + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Action is incompatible"`); }); test('should open flyout', async () => { + const overlays = coreMock.createStart().overlays; const spy = jest.spyOn(overlays, 'openFlyout'); - await drilldownAction.execute({ - embeddable: enhanceEmbeddable(new MockEmbeddable({ id: '' }, {})), + const action = createAction(overlays); + await action.execute({ + embeddable: compatibleEmbeddableApi, }); expect(spy).toBeCalled(); }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index aed94cd1c4adc..ebd4a6a3441e9 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -6,14 +6,28 @@ */ import React from 'react'; -import { distinctUntilChanged, filter, map, skip, take, takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; -import { Action } from '@kbn/ui-actions-plugin/public'; +import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { EmbeddableContext, ViewMode, CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; import { - isEnhancedEmbeddable, + tracksOverlays, + type PresentationContainer, + type TracksOverlays, +} from '@kbn/presentation-containers'; +import { + apiCanAccessViewMode, + apiHasSupportedTriggers, + getInheritedViewMode, + type CanAccessViewMode, + type EmbeddableApiContext, + type HasUniqueId, + type HasParentApi, + type HasSupportedTriggers, +} from '@kbn/presentation-publishing'; +import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; +import { + apiHasDynamicActions, embeddableEnhancedDrilldownGrouping, + type HasDynamicActions, } from '@kbn/embeddable-enhanced-plugin/public'; import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public'; import { txtDisplayName } from './i18n'; @@ -27,7 +41,16 @@ export interface FlyoutEditDrilldownParams { start: StartServicesGetter>; } -export class FlyoutEditDrilldownAction implements Action { +export type FlyoutEditDrilldownActionApi = CanAccessViewMode & + HasDynamicActions & + HasParentApi> & + HasSupportedTriggers & + Partial; + +const isApiCompatible = (api: unknown | null): api is FlyoutEditDrilldownActionApi => + apiHasDynamicActions(api) && apiCanAccessViewMode(api) && apiHasSupportedTriggers(api); + +export class FlyoutEditDrilldownAction implements Action { public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; public order = 10; @@ -45,32 +68,22 @@ export class FlyoutEditDrilldownAction implements Action { public readonly MenuItem = MenuItem as any; - public async isCompatible({ embeddable }: EmbeddableContext) { - if (!embeddable?.getInput) return false; - if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false; - if (!isEnhancedEmbeddable(embeddable)) return false; + public async isCompatible({ embeddable }: EmbeddableApiContext) { + if (!isApiCompatible(embeddable)) return false; + if (getInheritedViewMode(embeddable) !== 'edit') return false; return embeddable.enhancements.dynamicActions.state.get().events.length > 0; } - public async execute(context: EmbeddableContext) { + public async execute({ embeddable }: EmbeddableApiContext) { + if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); const { core, plugins } = this.params.start(); - const { embeddable } = context; - - if (!isEnhancedEmbeddable(embeddable)) { - throw new Error( - 'Need embeddable to be EnhancedEmbeddable to execute FlyoutEditDrilldownAction.' - ); - } const templates = createDrilldownTemplatesFromSiblings(embeddable); - const closed$ = new Subject(); + const overlayTracker = tracksOverlays(embeddable.parentApi) ? embeddable.parentApi : undefined; const close = () => { - closed$.next(true); + if (overlayTracker) overlayTracker.clearOverlays(); handle.close(); }; - const closeFlyout = () => { - close(); - }; const handle = core.overlays.openFlyout( toMountPoint( @@ -87,24 +100,12 @@ export class FlyoutEditDrilldownAction implements Action { { ownFocus: true, 'data-test-subj': 'editDrilldownFlyout', + onClose: () => { + close(); + }, } ); - // Close flyout on application change. - core.application.currentAppId$ - .pipe(takeUntil(closed$), skip(1), take(1)) - .subscribe(closeFlyout); - - // Close flyout on dashboard switch to "view" mode or on embeddable destroy. - embeddable - .getInput$() - .pipe( - takeUntil(closed$), - map((input) => input.viewMode), - distinctUntilChanged(), - filter((mode) => mode !== ViewMode.EDIT), - take(1) - ) - .subscribe({ next: closeFlyout, complete: closeFlyout }); + overlayTracker?.openOverlay(handle); } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts deleted file mode 100644 index 067b27fd12e5b..0000000000000 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts +++ /dev/null @@ -1,49 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Embeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; -import { EnhancedEmbeddable } from '@kbn/embeddable-enhanced-plugin/public'; -import { - UiActionsEnhancedMemoryActionStorage as MemoryActionStorage, - UiActionsEnhancedDynamicActionManager as DynamicActionManager, - AdvancedUiActionsStart, -} from '@kbn/ui-actions-enhanced-plugin/public'; -import { uiActionsEnhancedPluginMock } from '@kbn/ui-actions-enhanced-plugin/public/mocks'; - -export class MockEmbeddable extends Embeddable { - public rootType = 'dashboard'; - public readonly type = 'mock'; - private readonly triggers: string[] = []; - constructor(initialInput: EmbeddableInput, params: { supportedTriggers?: string[] }) { - super(initialInput, {}, undefined); - this.triggers = params.supportedTriggers ?? []; - } - public render(node: HTMLElement) {} - public reload() {} - public supportedTriggers(): string[] { - return this.triggers; - } - public getRoot() { - return { - type: this.rootType, - } as Embeddable; - } -} - -export const enhanceEmbeddable = ( - embeddable: E, - uiActions: AdvancedUiActionsStart = uiActionsEnhancedPluginMock.createStartContract() -): EnhancedEmbeddable => { - (embeddable as EnhancedEmbeddable).enhancements = { - dynamicActions: new DynamicActionManager({ - storage: new MemoryActionStorage(), - isCompatible: async () => true, - uiActions, - }), - }; - return embeddable as EnhancedEmbeddable; -}; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index e3bf4a4d468c8..00ca768a04848 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -6,15 +6,14 @@ */ import { Filter, RangeFilter, FilterStateStore, Query, TimeRange } from '@kbn/es-query'; -import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown'; +import { type Context, EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown'; import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown'; import { savedObjectsServiceMock } from '@kbn/core/public/mocks'; -import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { StartDependencies } from '../../../plugin'; import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public/core'; -import { EnhancedEmbeddableContext } from '@kbn/embeddable-enhanced-plugin/public'; import { DashboardAppLocatorDefinition } from '@kbn/dashboard-plugin/public/dashboard_app/locator/locator'; +import { BehaviorSubject } from 'rxjs'; describe('.isConfigValid()', () => { const drilldown = new EmbeddableToDashboardDrilldown({} as any); @@ -118,15 +117,18 @@ describe('.execute() & getHref', () => { const context = { filters: filtersFromEvent, embeddable: { - getInput: () => ({ - filters: [], - timeRange: { from: 'now-15m', to: 'now' }, - query: { query: 'test', language: 'kuery' }, - ...embeddableInput, - }), + parentApi: { + localFilters: new BehaviorSubject(embeddableInput.filters ? embeddableInput.filters : []), + localQuery: new BehaviorSubject( + embeddableInput.query ? embeddableInput.query : { query: 'test', language: 'kuery' } + ), + localTimeRange: new BehaviorSubject( + embeddableInput.timeRange ? embeddableInput.timeRange : { from: 'now-15m', to: 'now' } + ), + }, }, timeFieldName, - } as unknown as ApplyGlobalFilterActionContext & EnhancedEmbeddableContext; + } as Context; await drilldown.execute(completeConfig, context); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 0838297ee72e3..87790201b5099 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -5,6 +5,7 @@ * 2.0. */ import { extractTimeRange, isFilterPinned } from '@kbn/es-query'; +import type { HasParentApi, PublishesLocalUnifiedSearch } from '@kbn/presentation-publishing'; import type { KibanaLocation } from '@kbn/share-plugin/public'; import { cleanEmptyKeys, @@ -14,7 +15,6 @@ import { import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; -import { EnhancedEmbeddableContext } from '@kbn/embeddable-enhanced-plugin/public'; import { IMAGE_CLICK_TRIGGER } from '@kbn/image-embeddable-plugin/public'; import { AbstractDashboardDrilldown, @@ -24,7 +24,11 @@ import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; import { createExtract, createInject } from '../../../../common'; import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown'; -type Context = EnhancedEmbeddableContext & ApplyGlobalFilterActionContext; +export type Context = ApplyGlobalFilterActionContext & { + embeddable: Partial< + PublishesLocalUnifiedSearch & HasParentApi> + >; +}; export type Params = AbstractDashboardDrilldownParams; /** diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json index 4c08a46b6e2d6..0125f8c463513 100644 --- a/x-pack/plugins/dashboard_enhanced/tsconfig.json +++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json @@ -19,7 +19,9 @@ "@kbn/unified-search-plugin", "@kbn/ui-actions-plugin", "@kbn/image-embeddable-plugin", - "@kbn/presentation-util-plugin" + "@kbn/presentation-util-plugin", + "@kbn/presentation-containers", + "@kbn/presentation-publishing" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts index 86ca2030d2ae7..1e896817b3242 100644 --- a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts +++ b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts @@ -6,12 +6,9 @@ */ import { i18n } from '@kbn/i18n'; -import { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { UiActionsPresentableGrouping as PresentableGrouping } from '@kbn/ui-actions-plugin/public'; -export const drilldownGrouping: PresentableGrouping<{ - embeddable?: IEmbeddable; -}> = [ +export const drilldownGrouping: PresentableGrouping = [ { id: 'drilldowns', getDisplayName: () => diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/interfaces/has_dynamic_actions.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/interfaces/has_dynamic_actions.ts new file mode 100644 index 0000000000000..d92fd2138b56b --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/interfaces/has_dynamic_actions.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '@kbn/ui-actions-enhanced-plugin/public'; + +export interface HasDynamicActions { + enhancements: { + dynamicActions: DynamicActionManager; + }; +} + +export const apiHasDynamicActions = (api: unknown): api is HasDynamicActions => { + return Boolean( + api && typeof (api as HasDynamicActions).enhancements?.dynamicActions === 'object' + ); +}; diff --git a/x-pack/plugins/embeddable_enhanced/public/index.ts b/x-pack/plugins/embeddable_enhanced/public/index.ts index 63bd6d17ddcb9..53fec5c83522a 100644 --- a/x-pack/plugins/embeddable_enhanced/public/index.ts +++ b/x-pack/plugins/embeddable_enhanced/public/index.ts @@ -21,4 +21,8 @@ export function plugin(context: PluginInitializerContext) { export type { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types'; export { isEnhancedEmbeddable } from './embeddables'; +export { + type HasDynamicActions, + apiHasDynamicActions, +} from './embeddables/interfaces/has_dynamic_actions'; export { drilldownGrouping as embeddableEnhancedDrilldownGrouping } from './actions'; diff --git a/x-pack/plugins/embeddable_enhanced/public/types.ts b/x-pack/plugins/embeddable_enhanced/public/types.ts index 7500a37e120cc..fd5a0f689f74b 100644 --- a/x-pack/plugins/embeddable_enhanced/public/types.ts +++ b/x-pack/plugins/embeddable_enhanced/public/types.ts @@ -17,6 +17,9 @@ export type EnhancedEmbeddable = E & { }; }; +/** + * @deprecated use `EmbeddableApiContext` from `@kbn/presentation-publishing` + */ export interface EnhancedEmbeddableContext { embeddable: EnhancedEmbeddable; }