diff --git a/src/platform/plugins/shared/dashboard/test/scout/ui/parallel_tests/dashboard_panel_listing_obs_group.spec.ts b/src/platform/plugins/shared/dashboard/test/scout/ui/parallel_tests/dashboard_panel_listing_obs_group.spec.ts index 3455b3b793b77..5ea584baa8c31 100644 --- a/src/platform/plugins/shared/dashboard/test/scout/ui/parallel_tests/dashboard_panel_listing_obs_group.spec.ts +++ b/src/platform/plugins/shared/dashboard/test/scout/ui/parallel_tests/dashboard_panel_listing_obs_group.spec.ts @@ -21,7 +21,7 @@ const DASHBOARD_PANEL_GROUP_ORDER = [ 'legacyGroup', ]; -const DASHBOARD_PANEL_TYPE_COUNT = 24; +const DASHBOARD_PANEL_TYPE_COUNT = 25; spaceTest.describe( 'Dashboard panel listing (includes observability group)', diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/use_top_nav_links.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/use_top_nav_links.tsx index 284cfbc50be95..067ce476bf036 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/use_top_nav_links.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/use_top_nav_links.tsx @@ -47,6 +47,7 @@ import { import type { DiscoverAppState } from '../../state_management/redux'; import { onSaveDiscoverSession } from './save_discover_session'; import { useDataState } from '../../hooks/use_data_state'; +import { TransferAction } from '../../../../plugin_imports/embeddable_editor_service'; /** * Helper function to build the top nav links @@ -275,7 +276,15 @@ export const useTopNavLinks = ({ await onSaveDiscoverSession({ services, state, - onSaveCb: isEmbeddedEditor ? services.embeddableEditor.transferBackToEditor : undefined, + onSaveCb: isEmbeddedEditor + ? (saveState) => { + const action = saveState + ? TransferAction.SaveSession + : TransferAction.SaveByValue; + + services.embeddableEditor.transferBackToEditor(action, saveState); + } + : undefined, }); }, popoverWidth: 150, @@ -291,7 +300,8 @@ export const useTopNavLinks = ({ items: [ savedAsButton, { - run: () => services.embeddableEditor.transferBackToEditor(), + run: () => + services.embeddableEditor.transferBackToEditor(TransferAction.Cancel), id: 'cancel', order: 100, label: i18n.translate('discover.localMenu.cancelTitle', { diff --git a/src/platform/plugins/shared/discover/public/embeddable/actions/add_discover_session_panel_action.ts b/src/platform/plugins/shared/discover/public/embeddable/actions/add_discover_session_panel_action.ts new file mode 100644 index 0000000000000..94597fdf97da5 --- /dev/null +++ b/src/platform/plugins/shared/discover/public/embeddable/actions/add_discover_session_panel_action.ts @@ -0,0 +1,96 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { i18n } from '@kbn/i18n'; +import { v4 as uuidv4 } from 'uuid'; +import type { ApplicationStart } from '@kbn/core/public'; +import { + type EmbeddableApiContext, + apiCanAccessViewMode, + apiHasAppContext, + apiIsOfType, + getInheritedViewMode, +} from '@kbn/presentation-publishing'; + +import type { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; +import { ADD_PANEL_VISUALIZATION_GROUP, type EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { DiscoverSessionTab } from '@kbn/saved-search-plugin/common'; +import type { DiscoverAppLocator } from '../../../common'; +import { ACTION_ADD_DISCOVER_SESSION_PANEL } from '../constants'; + +export class AddDiscoverSessionPanelAction implements Action { + public id = ACTION_ADD_DISCOVER_SESSION_PANEL; + public readonly type = ACTION_ADD_DISCOVER_SESSION_PANEL; + public readonly order = 45; + public readonly grouping = [ADD_PANEL_VISUALIZATION_GROUP]; + + constructor( + private readonly application: ApplicationStart, + private readonly locator: DiscoverAppLocator, + private readonly embeddable: EmbeddableStart + ) {} + + getIconType(): string | undefined { + return 'discoverApp'; + } + + getDisplayName(): string { + return i18n.translate('discover.savedSearchEmbeddable.action.addPanel.displayName', { + defaultMessage: 'Discover session', + }); + } + + async execute({ + embeddable: embeddableApi, + }: ActionExecutionContext): Promise { + const { app, path } = await this.locator.getLocation({}); + const stateTransfer = this.embeddable.getStateTransfer(); + + const valueInput: DiscoverSessionTab = { + id: uuidv4(), + label: i18n.translate('discover.savedSearchEmbeddable.action.addPanel.byValueTabName', { + defaultMessage: 'New by-value Discover session', + }), + sort: [], + columns: [], + isTextBasedQuery: true, + grid: {}, + hideChart: false, + serializedSearchSource: {}, + }; + + const appContext = apiHasAppContext(embeddableApi) ? embeddableApi.getAppContext() : undefined; + + stateTransfer.navigateToEditor(app, { + path, + state: { + valueInput, + originatingApp: appContext?.currentAppId || '', + originatingPath: appContext?.getCurrentPath?.(), + }, + }); + } + + getDisplayNameTooltip(): string { + return ''; + } + + async isCompatible({ embeddable }: ActionExecutionContext) { + const { capabilities } = this.application; + const hasDiscoverPermissions = + (capabilities.discover_v2.show as boolean) || (capabilities.discover_v2.save as boolean); + + if (!hasDiscoverPermissions) return false; + + return ( + apiCanAccessViewMode(embeddable) && + getInheritedViewMode(embeddable) === 'edit' && + apiIsOfType(embeddable, 'dashboard') + ); + } +} diff --git a/src/platform/plugins/shared/discover/public/embeddable/constants.ts b/src/platform/plugins/shared/discover/public/embeddable/constants.ts index 00b750f8838f0..1a557c8b27057 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/constants.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/constants.ts @@ -18,6 +18,8 @@ export const LEGACY_LOG_STREAM_EMBEDDABLE = 'log_stream'; export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH'; +export const ACTION_ADD_DISCOVER_SESSION_PANEL = 'ACTION_ADD_DISCOVER_SESSION_PANEL'; + export const DEFAULT_HEADER_ROW_HEIGHT_LINES = 3; /** This constant refers to the dashboard panel specific state */ diff --git a/src/platform/plugins/shared/discover/public/plugin.tsx b/src/platform/plugins/shared/discover/public/plugin.tsx index b7659e05e1d30..e7a64b87d5347 100644 --- a/src/platform/plugins/shared/discover/public/plugin.tsx +++ b/src/platform/plugins/shared/discover/public/plugin.tsx @@ -26,7 +26,7 @@ import type { SavedSearchAttributes } from '@kbn/saved-search-plugin/common'; import { i18n } from '@kbn/i18n'; import { once } from 'lodash'; import { DISCOVER_ESQL_LOCATOR } from '@kbn/deeplinks-analytics'; -import { ON_OPEN_PANEL_MENU } from '@kbn/ui-actions-plugin/common/trigger_ids'; +import { ADD_PANEL_TRIGGER, ON_OPEN_PANEL_MENU } from '@kbn/ui-actions-plugin/common/trigger_ids'; import type { DrilldownTransforms } from '@kbn/embeddable-plugin/common'; import { ProjectRoutingAccess } from '@kbn/cps-utils'; import { DISCOVER_APP_LOCATOR, PLUGIN_ID, type DiscoverAppLocator } from '../common'; @@ -42,7 +42,11 @@ import { registerFeature } from './plugin_imports/register_feature'; import type { UrlTracker } from './build_services'; import { initializeKbnUrlTracking } from './utils/initialize_kbn_url_tracking'; import { defaultCustomizationContext } from './customizations/defaults'; -import { ACTION_VIEW_SAVED_SEARCH, LEGACY_LOG_STREAM_EMBEDDABLE } from './embeddable/constants'; +import { + ACTION_ADD_DISCOVER_SESSION_PANEL, + ACTION_VIEW_SAVED_SEARCH, + LEGACY_LOG_STREAM_EMBEDDABLE, +} from './embeddable/constants'; import { DiscoverContainerInternal, type DiscoverContainerProps, @@ -248,6 +252,19 @@ export class DiscoverPlugin } ); + plugins.uiActions.addTriggerActionAsync( + ADD_PANEL_TRIGGER, + ACTION_ADD_DISCOVER_SESSION_PANEL, + async () => { + const { AddDiscoverSessionPanelAction } = await getEmbeddableServices(); + return new AddDiscoverSessionPanelAction( + core.application, + this.locator!, + plugins.embeddable + ); + } + ); + const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL); if (plugins.share && this.locator && isEsqlEnabled) { diff --git a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_editor_service.ts b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_editor_service.ts index 99387eb0af543..337d91a679c79 100644 --- a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_editor_service.ts +++ b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_editor_service.ts @@ -13,6 +13,24 @@ import type { import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils'; import type { EmbeddableEditorState, EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; +/** + * Specifies the action to be taken for navigating back to an editor. + */ +export enum TransferAction { + /** + * A Cancel operation. Returns to the editor without modifying the original state. + */ + Cancel, + /** + * A Save Session operation. Updates the saved session and doesn't pass back any serialised state. + */ + SaveSession, + /** + * A Save By Value operation. Sends back to the editor the serialised updated state for the embeddable. + */ + SaveByValue, +} + export class EmbeddableEditorService { private embeddableState?: EmbeddableEditorState; @@ -27,6 +45,9 @@ export class EmbeddableEditorService { public getByValueInput = (): DiscoverSessionTab | undefined => this.embeddableState?.valueInput as DiscoverSessionTab | undefined; + /** + * Resets the embeddable transfer state, ensuring it is cleared in storage and then dropped in memory. + */ public clearEditorState = () => { if (this.embeddableState) { this.embeddableStateTransfer.clearEditorState('discover'); @@ -34,7 +55,19 @@ export class EmbeddableEditorService { } }; - public transferBackToEditor = (state?: SavedSearchByValueAttributes) => { + public transferBackToEditor(action: TransferAction.Cancel | TransferAction.SaveSession): void; + public transferBackToEditor( + action: TransferAction.SaveByValue, + state: SavedSearchByValueAttributes + ): void; + public transferBackToEditor(action: TransferAction, state?: SavedSearchByValueAttributes): void; + /** + * Initiates a navigation back to the editing application, either cancelling the current action to return + * or passing a state for an embeddable to receive an updated view. + * + * **NOTE**: Cancelling will never pass an updated state, so the state param is ignored for cancel actions. + */ + public transferBackToEditor(action: TransferAction, state?: SavedSearchByValueAttributes) { if (this.embeddableState) { const app = this.embeddableState.originatingApp; const path = this.embeddableState.originatingPath; @@ -43,15 +76,18 @@ export class EmbeddableEditorService { this.embeddableStateTransfer.clearEditorState('discover'); this.embeddableStateTransfer.navigateToWithEmbeddablePackages(app, { path, - state: [ - { - type: SEARCH_EMBEDDABLE_TYPE, - serializedState: { attributes: state }, - embeddableId: this.embeddableState?.embeddableId, - }, - ], + state: + action !== TransferAction.Cancel + ? [ + { + type: SEARCH_EMBEDDABLE_TYPE, + serializedState: { attributes: state }, + embeddableId: this.embeddableState?.embeddableId, + }, + ] + : [], }); } } - }; + } } diff --git a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts index 2f261daaae701..4b3fd79185e74 100644 --- a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts +++ b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts @@ -8,6 +8,7 @@ */ export { ViewSavedSearchAction } from '../embeddable/actions/view_saved_search_action'; +export { AddDiscoverSessionPanelAction } from '../embeddable/actions/add_discover_session_panel_action'; export { addPanelFromLibrary } from '../embeddable/utils/add_panel_from_library'; export { getSearchEmbeddableFactory } from '../embeddable/get_search_embeddable_factory'; export { getLegacyLogStreamEmbeddableFactory } from '../embeddable/get_legacy_log_stream_embeddable_factory'; diff --git a/src/platform/plugins/shared/discover/public/utils/breadcrumbs.ts b/src/platform/plugins/shared/discover/public/utils/breadcrumbs.ts index 746dca8129c3a..663ca56d51529 100644 --- a/src/platform/plugins/shared/discover/public/utils/breadcrumbs.ts +++ b/src/platform/plugins/shared/discover/public/utils/breadcrumbs.ts @@ -10,7 +10,10 @@ import { i18n } from '@kbn/i18n'; import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; import type { DiscoverServices } from '../build_services'; -import type { EmbeddableEditorService } from '../plugin_imports/embeddable_editor_service'; +import { + TransferAction, + type EmbeddableEditorService, +} from '../plugin_imports/embeddable_editor_service'; const rootPath = '#/'; @@ -36,7 +39,9 @@ function getRootBreadcrumbs({ }), deepLinkId: isEmbeddedEditor ? 'dashboards' : 'discover', href, - onClick: isEmbeddedEditor ? () => embeddable.transferBackToEditor() : undefined, + onClick: isEmbeddedEditor + ? () => embeddable.transferBackToEditor(TransferAction.Cancel) + : undefined, }, ]; } diff --git a/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts b/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts index 8541739ff65f0..a2d4aabf1ae89 100644 --- a/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts +++ b/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts @@ -72,7 +72,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); // Any changes to the number of panels needs to be audited by @elastic/kibana-presentation - expect(panelTypes.length).to.eql(14); + expect(panelTypes.length).to.eql(15); }); }); } diff --git a/src/platform/test/functional/apps/discover/embeddable/_new_panel_embeddable.ts b/src/platform/test/functional/apps/discover/embeddable/_new_panel_embeddable.ts new file mode 100644 index 0000000000000..b5b359892845c --- /dev/null +++ b/src/platform/test/functional/apps/discover/embeddable/_new_panel_embeddable.ts @@ -0,0 +1,105 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dashboardAddPanel = getService('dashboardAddPanel'); + const filterBar = getService('filterBar'); + const queryBar = getService('queryBar'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const globalNav = getService('globalNav'); + const { common, dashboard, header, discover } = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'discover', + ]); + + describe('add new discover panel embeddable', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'src/platform/test/functional/fixtures/es_archiver/logstash_functional' + ); + await esArchiver.loadIfNeeded( + 'src/platform/test/functional/fixtures/es_archiver/dashboard/current/data' + ); + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'src/platform/test/functional/fixtures/kbn_archiver/dashboard/current/kibana' + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await common.setTime({ + from: 'Sep 22, 2015 @ 00:00:00.000', + to: 'Sep 23, 2015 @ 00:00:00.000', + }); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await common.unsetTime(); + await kibanaServer.uiSettings.unset('defaultIndex'); + }); + + beforeEach(async () => { + await dashboard.navigateToApp(); + await filterBar.ensureFieldEditorModalIsClosed(); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + }); + + it('can add a new Discover session panel to the dashboard', async () => { + await dashboardAddPanel.clickAddDiscoverPanel(); + await header.waitUntilLoadingHasFinished(); + await Promise.all([ + globalNav + .getFirstBreadcrumb() + .then((firstBreadcrumb) => expect(firstBreadcrumb).to.be('Dashboards')), + discover + .getSavedSearchTitle() + .then((lastBreadcrumb) => + expect(lastBreadcrumb).to.be('Editing New by-value Discover session') + ), + testSubjects + .exists('unifiedTabs_tabsBar', { timeout: 1000 }) + .then((unifiedTabs) => expect(unifiedTabs).not.to.be(true)), + discover.isOnDashboardsEditMode().then((editMode) => expect(editMode).to.be(true)), + ]); + + await queryBar.setQuery('test'); + await queryBar.submitQuery(); + await discover.waitUntilTabIsLoaded(); + await discover.clickSaveSearchButton(); + await dashboard.waitForRenderComplete(); + await dashboard.verifyNoRenderErrors(); + expect(await discover.getAllSavedSearchDocumentCount()).to.eql(['13 documents']); + }); + + it('can cancel adding a new Discover session panel', async () => { + await dashboardAddPanel.clickAddDiscoverPanel(); + await header.waitUntilLoadingHasFinished(); + + await queryBar.setQuery('test'); + await queryBar.submitQuery(); + await discover.waitUntilTabIsLoaded(); + + await discover.clickCancelButton(); + + await dashboard.waitForRenderComplete(); + await dashboard.verifyNoRenderErrors(); + + expect(await discover.getAllSavedSearchDocumentCount()).to.eql([]); + }); + }); +} diff --git a/src/platform/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts b/src/platform/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts index 1dcb2a14c73c7..33c5874275142 100644 --- a/src/platform/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts +++ b/src/platform/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts @@ -54,6 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await kibanaServer.savedObjects.cleanStandardList(); await common.unsetTime(); + await kibanaServer.uiSettings.unset('defaultIndex'); }); beforeEach(async () => { diff --git a/src/platform/test/functional/apps/discover/embeddable/index.ts b/src/platform/test/functional/apps/discover/embeddable/index.ts index 1663bf4181dc3..d0de1bb020fe7 100644 --- a/src/platform/test/functional/apps/discover/embeddable/index.ts +++ b/src/platform/test/functional/apps/discover/embeddable/index.ts @@ -28,5 +28,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./multiple_data_views')); loadTestFile(require.resolve('./_log_stream_embeddable.ts')); loadTestFile(require.resolve('./_esql_embeddable.ts')); + loadTestFile(require.resolve('./_new_panel_embeddable.ts')); }); } diff --git a/src/platform/test/functional/services/dashboard/add_panel.ts b/src/platform/test/functional/services/dashboard/add_panel.ts index 83fc5d08f8942..83839f0023961 100644 --- a/src/platform/test/functional/services/dashboard/add_panel.ts +++ b/src/platform/test/functional/services/dashboard/add_panel.ts @@ -76,6 +76,12 @@ export class DashboardAddPanelService extends FtrService { await this.clickAddNewPanelFromUIActionLink('ES|QL'); } + async clickAddDiscoverPanel() { + this.log.debug('DashboardAddPanel.clickAddDiscoverPanel'); + await this.openAddPanelFlyout(); + await this.clickAddNewPanelFromUIActionLink('Discover session'); + } + async openAddPanelFlyout() { this.log.debug('DashboardAddPanel.openAddPanelFlyout'); await this.clickTopNavAddMenu();