diff --git a/src/platform/plugins/shared/discover/kibana.jsonc b/src/platform/plugins/shared/discover/kibana.jsonc index d64f48f59ba4a..8a7f48ca003ac 100644 --- a/src/platform/plugins/shared/discover/kibana.jsonc +++ b/src/platform/plugins/shared/discover/kibana.jsonc @@ -48,7 +48,8 @@ "observabilityAIAssistant", "aiops", "fieldsMetadata", - "logsDataAccess" + "logsDataAccess", + "embeddableEnhanced" ], "requiredBundles": [ "kibanaUtils", diff --git a/src/platform/plugins/shared/discover/public/build_services.ts b/src/platform/plugins/shared/discover/public/build_services.ts index 0aef34338c374..463fccfff38fc 100644 --- a/src/platform/plugins/shared/discover/public/build_services.ts +++ b/src/platform/plugins/shared/discover/public/build_services.ts @@ -62,6 +62,7 @@ import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/publ import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; +import type { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public'; import type { DiscoverStartPlugins } from './types'; import type { DiscoverContextAppLocator } from './application/context/services/locator'; import type { DiscoverSingleDocLocator } from './application/doc/locator'; @@ -140,6 +141,7 @@ export interface DiscoverServices { ebtManager: DiscoverEBTManager; fieldsMetadata?: FieldsMetadataPublicStart; logsDataAccess?: LogsDataAccessPluginStart; + embeddableEnhanced?: EmbeddableEnhancedPluginStart; } export const buildServices = memoize( @@ -233,6 +235,7 @@ export const buildServices = memoize( ebtManager, fieldsMetadata: plugins.fieldsMetadata, logsDataAccess: plugins.logsDataAccess, + embeddableEnhanced: plugins.embeddableEnhanced, }; } ); diff --git a/src/platform/plugins/shared/discover/public/embeddable/constants.ts b/src/platform/plugins/shared/discover/public/embeddable/constants.ts index 938caa233b435..cfccf18bf45ed 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/constants.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/constants.ts @@ -7,8 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { SavedSearchAttributes } from '@kbn/saved-search-plugin/common'; +import type { SavedSearchAttributes } from '@kbn/saved-search-plugin/common'; import type { Trigger } from '@kbn/ui-actions-plugin/public'; +import type { SearchEmbeddableSerializedState } from './types'; export { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils'; @@ -37,9 +38,10 @@ export const EDITABLE_SAVED_SEARCH_KEYS: Readonly> = [ 'title', // panel title 'description', // panel description 'timeRange', // panel custom time range 'hidePanelTitles', // panel hidden title + 'enhancements', // panel enhancements (e.g. drilldowns) ] as const; diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx index b500e5a0558fa..63e00bad6d5e3 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx @@ -97,6 +97,13 @@ export const getSearchEmbeddableFactory = ({ /** Build API */ const titleManager = initializeTitleManager(initialState); const timeRange = initializeTimeRange(initialState); + const dynamicActionsApi = + discoverServices.embeddableEnhanced?.initializeReactEmbeddableDynamicActions( + uuid, + () => titleManager.api.title$.getValue(), + initialState + ); + const maybeStopDynamicActions = dynamicActionsApi?.startDynamicActions(); const searchEmbeddable = await initializeSearchEmbeddableApi(initialState, { discoverServices, }); @@ -126,6 +133,7 @@ export const getSearchEmbeddableFactory = ({ savedSearch: searchEmbeddable.api.savedSearch$.getValue(), serializeTitles: titleManager.serialize, serializeTimeRange: timeRange.serialize, + serializeDynamicActions: dynamicActionsApi?.serializeDynamicActions, savedObjectId, }); @@ -134,6 +142,7 @@ export const getSearchEmbeddableFactory = ({ ...titleManager.api, ...searchEmbeddable.api, ...timeRange.api, + ...dynamicActionsApi?.dynamicActionsApi, ...initializeEditApi({ uuid, parentApi, @@ -178,10 +187,18 @@ export const getSearchEmbeddableFactory = ({ getSerializedStateByReference: (newId: string) => serialize(newId), serializeState: () => serialize(savedObjectId$.getValue()), getInspectorAdapters: () => searchEmbeddable.stateManager.inspectorAdapters.getValue(), + supportedTriggers: () => { + // No triggers are supported, but this is still required to pass the drilldown + // compatibilty check and ensure top-level drilldowns (e.g. URL) work as expected + return []; + }, }, { ...titleManager.comparators, ...timeRange.comparators, + ...(dynamicActionsApi?.dynamicActionsComparator ?? { + enhancements: getUnchangingComparator(), + }), ...searchEmbeddable.comparators, rawSavedObjectAttributes: getUnchangingComparator(), savedObjectId: [savedObjectId$, (value) => savedObjectId$.next(value)], @@ -206,6 +223,7 @@ export const getSearchEmbeddableFactory = ({ return () => { searchEmbeddable.cleanup(); unsubscribeFromFetch(); + maybeStopDynamicActions?.stopDynamicActions(); }; }, []); diff --git a/src/platform/plugins/shared/discover/public/embeddable/types.ts b/src/platform/plugins/shared/discover/public/embeddable/types.ts index 3598552e65d2d..25fc6bda48c7f 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/types.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/types.ts @@ -7,13 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { DataTableRecord } from '@kbn/discover-utils/types'; +import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; -import { HasInspectorAdapters } from '@kbn/inspector-plugin/public'; -import { +import type { HasInspectorAdapters } from '@kbn/inspector-plugin/public'; +import type { EmbeddableApiContext, HasEditCapabilities, HasLibraryTransforms, + HasSupportedTriggers, PublishesBlockingError, PublishesDataLoading, PublishesSavedObjectId, @@ -23,15 +24,17 @@ import { SerializedTimeRange, SerializedTitles, } from '@kbn/presentation-publishing'; -import { +import type { SavedSearch, SavedSearchAttributes, SerializableSavedSearch, } from '@kbn/saved-search-plugin/common/types'; -import { DataTableColumnsMeta } from '@kbn/unified-data-table'; -import { BehaviorSubject } from 'rxjs'; -import { PublishesWritableDataViews } from '@kbn/presentation-publishing/interfaces/publishes_data_views'; -import { EDITABLE_SAVED_SEARCH_KEYS } from './constants'; +import type { DataTableColumnsMeta } from '@kbn/unified-data-table'; +import type { BehaviorSubject } from 'rxjs'; +import type { PublishesWritableDataViews } from '@kbn/presentation-publishing/interfaces/publishes_data_views'; +import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin'; +import type { HasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public'; +import type { EDITABLE_SAVED_SEARCH_KEYS } from './constants'; export type SearchEmbeddableState = Pick< SerializableSavedSearch, @@ -75,6 +78,7 @@ export type EditableSavedSearchAttributes = Partial< export type SearchEmbeddableSerializedState = SerializedTitles & SerializedTimeRange & + Partial & EditableSavedSearchAttributes & { // by value attributes?: SavedSearchAttributes & { references: SavedSearch['references'] }; @@ -85,7 +89,8 @@ export type SearchEmbeddableSerializedState = SerializedTitles & export type SearchEmbeddableRuntimeState = SearchEmbeddableSerializedAttributes & SerializedTitles & - SerializedTimeRange & { + SerializedTimeRange & + Partial & { rawSavedObjectAttributes?: EditableSavedSearchAttributes; savedObjectTitle?: string; savedObjectId?: string; @@ -107,7 +112,9 @@ export type SearchEmbeddableApi = DefaultEmbeddableApi< HasLibraryTransforms & HasTimeRange & HasInspectorAdapters & - Partial; + Partial & + HasDynamicActions & + HasSupportedTriggers; export interface PublishesSavedSearch { savedSearch$: PublishingSubject; diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts index a4ab454c8a49c..3bc9e5dd2a396 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.test.ts @@ -121,6 +121,7 @@ describe('Serialization utils', () => { savedSearch, serializeTitles: jest.fn(), serializeTimeRange: jest.fn(), + serializeDynamicActions: jest.fn(), }); expect(serializedState).toEqual({ @@ -156,6 +157,7 @@ describe('Serialization utils', () => { savedSearch, serializeTitles: jest.fn(), serializeTimeRange: jest.fn(), + serializeDynamicActions: jest.fn(), savedObjectId: 'test-id', }); @@ -176,6 +178,7 @@ describe('Serialization utils', () => { savedSearch: { ...savedSearch, sampleSize: 500, sort: [['order_date', 'asc']] }, serializeTitles: jest.fn(), serializeTimeRange: jest.fn(), + serializeDynamicActions: jest.fn(), savedObjectId: 'test-id', }); diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts index fb6eeed18f85f..2342cf793bcfc 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts @@ -23,6 +23,7 @@ import { } from '@kbn/saved-search-plugin/common'; import { SavedSearchUnwrapResult } from '@kbn/saved-search-plugin/public'; +import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin'; import { extract, inject } from '../../../common/embeddable/search_inject_extract'; import { DiscoverServices } from '../../build_services'; import { @@ -85,6 +86,7 @@ export const serializeState = ({ savedSearch, serializeTitles, serializeTimeRange, + serializeDynamicActions, savedObjectId, }: { uuid: string; @@ -92,6 +94,7 @@ export const serializeState = ({ savedSearch: SavedSearch; serializeTitles: () => SerializedTitles; serializeTimeRange: () => SerializedTimeRange; + serializeDynamicActions: (() => DynamicActionsSerializedState) | undefined; savedObjectId?: string; }): SerializedPanelState => { const searchSource = savedSearch.searchSource; @@ -115,6 +118,7 @@ export const serializeState = ({ // Serialize the current dashboard state into the panel state **without** updating the saved object ...serializeTitles(), ...serializeTimeRange(), + ...serializeDynamicActions?.(), ...overwriteState, }, // No references to extract for by-reference embeddable since all references are stored with by-reference saved object diff --git a/src/platform/plugins/shared/discover/public/types.ts b/src/platform/plugins/shared/discover/public/types.ts index 588be70a5baba..fdf4a4da60efa 100644 --- a/src/platform/plugins/shared/discover/public/types.ts +++ b/src/platform/plugins/shared/discover/public/types.ts @@ -43,6 +43,7 @@ import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/publ import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; +import type { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public'; import type { DiscoverAppLocator } from '../common'; import { type DiscoverContainerProps } from './components/discover_container'; @@ -168,4 +169,5 @@ export interface DiscoverStartPlugins { unifiedSearch: UnifiedSearchPublicPluginStart; urlForwarding: UrlForwardingStart; usageCollection?: UsageCollectionSetup; + embeddableEnhanced?: EmbeddableEnhancedPluginStart; } diff --git a/src/platform/plugins/shared/discover/tsconfig.json b/src/platform/plugins/shared/discover/tsconfig.json index ad6498e95171d..b0b38592c2c18 100644 --- a/src/platform/plugins/shared/discover/tsconfig.json +++ b/src/platform/plugins/shared/discover/tsconfig.json @@ -98,7 +98,8 @@ "@kbn/logs-data-access-plugin", "@kbn/core-lifecycle-browser", "@kbn/esql-ast", - "@kbn/discover-shared-plugin" + "@kbn/discover-shared-plugin", + "@kbn/embeddable-enhanced-plugin" ], "exclude": ["target/**/*"] } diff --git a/test/functional/services/dashboard/drilldowns_manage.ts b/test/functional/services/dashboard/drilldowns_manage.ts index c02728fc41f88..0f456c8e485b5 100644 --- a/test/functional/services/dashboard/drilldowns_manage.ts +++ b/test/functional/services/dashboard/drilldowns_manage.ts @@ -78,7 +78,11 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon }: { drilldownName: string; destinationURLTemplate: string; - trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER' | 'IMAGE_CLICK_TRIGGER'; + trigger: + | 'VALUE_CLICK_TRIGGER' + | 'SELECT_RANGE_TRIGGER' + | 'IMAGE_CLICK_TRIGGER' + | 'CONTEXT_MENU_TRIGGER'; }) { await this.fillInDrilldownName(drilldownName); await this.selectTriggerIfNeeded(trigger); @@ -94,7 +98,11 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon } async selectTriggerIfNeeded( - trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER' | 'IMAGE_CLICK_TRIGGER' + trigger: + | 'VALUE_CLICK_TRIGGER' + | 'SELECT_RANGE_TRIGGER' + | 'IMAGE_CLICK_TRIGGER' + | 'CONTEXT_MENU_TRIGGER' ) { if (await testSubjects.exists(`triggerPicker`)) { const container = await testSubjects.find(`triggerPicker-${trigger}`); diff --git a/x-pack/test/functional/apps/discover/saved_search_embeddable.ts b/x-pack/test/functional/apps/discover/saved_search_embeddable.ts index ea00343628258..8d012aa402878 100644 --- a/x-pack/test/functional/apps/discover/saved_search_embeddable.ts +++ b/x-pack/test/functional/apps/discover/saved_search_embeddable.ts @@ -13,11 +13,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); const filterBar = getService('filterBar'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - const { common, dashboard, header } = getPageObjects(['common', 'dashboard', 'header']); + const find = getService('find'); + const queryBar = getService('queryBar'); + const { common, dashboard, header, discover } = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'discover', + ]); describe('discover saved search embeddable', () => { before(async () => { @@ -106,5 +115,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('embeddableError'); }); + + it('should support URL drilldown', async () => { + await addSearchEmbeddableToDashboard(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + const drilldownName = 'URL drilldown'; + const urlTemplate = + "{{kibanaUrl}}/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'{{context.panel.timeRange.from}}',to:'{{context.panel.timeRange.to}}'))" + + "&_a=(columns:!(_source),filters:{{rison context.panel.filters}},index:'{{context.panel.indexPatternId}}',interval:auto," + + "query:(language:{{context.panel.query.language}},query:'clientip:239.190.189.77'),sort:!())"; + await testSubjects.click('actionFactoryItem-URL_DRILLDOWN'); + await dashboardDrilldownsManage.fillInDashboardToURLDrilldownWizard({ + drilldownName, + destinationURLTemplate: urlTemplate, + trigger: 'CONTEXT_MENU_TRIGGER', + }); + await testSubjects.click('urlDrilldownAdditionalOptions'); + await testSubjects.click('urlDrilldownOpenInNewTab'); + await dashboardDrilldownsManage.saveChanges(); + await dashboard.saveDashboard('Dashboard with URL drilldown', { + saveAsNew: true, + waitDialogIsClosed: true, + exitFromEditMode: true, + }); + await browser.refresh(); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + await dashboardPanelActions.openContextMenu(); + await find.clickByLinkText(drilldownName); + await discover.waitForDiscoverAppOnScreen(); + await header.waitUntilLoadingHasFinished(); + await discover.waitForDocTableLoadingComplete(); + expect(await queryBar.getQueryString()).to.be('clientip:239.190.189.77'); + expect(await discover.getHitCount()).to.be('6'); + }); }); }