@@ -453,7 +479,11 @@ export function App({ {isSaveModalVisible && ( { @@ -466,13 +496,15 @@ export function App({ initialInput={initialInput} redirectTo={redirectTo} redirectToOrigin={redirectToOrigin} + initialContext={initialContext} returnToOriginSwitchLabel={ - getIsByValueMode() && initialInput + returnToOriginSwitchLabelForContext ?? + (getIsByValueMode() && initialInput ? i18n.translate('xpack.lens.app.updatePanel', { defaultMessage: 'Update panel on {originatingAppName}', values: { originatingAppName: getOriginatingAppName() }, }) - : undefined + : undefined) } /> )} diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 582741fe68741..54bae8b037847 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -41,6 +41,7 @@ import { } from '../utils'; import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; import { changeIndexPattern } from '../state_management/lens_slice'; +import { LensByReferenceInput } from '../embeddable'; function getLensTopNavConfig(options: { showSaveAndReturn: boolean; @@ -55,6 +56,9 @@ function getLensTopNavConfig(options: { savingToDashboardPermitted: boolean; contextOriginatingApp?: string; isSaveable: boolean; + showReplaceInDashboard: boolean; + showReplaceInCanvas: boolean; + contextFromEmbeddable?: boolean; }): TopNavMenuData[] { const { actions, @@ -68,6 +72,9 @@ function getLensTopNavConfig(options: { tooltips, contextOriginatingApp, isSaveable, + showReplaceInDashboard, + showReplaceInCanvas, + contextFromEmbeddable, } = options; const topNavMenu: TopNavMenuData[] = []; @@ -90,14 +97,14 @@ function getLensTopNavConfig(options: { defaultMessage: 'Save', }); - if (contextOriginatingApp) { + if (contextOriginatingApp && !showCancel) { topNavMenu.push({ label: i18n.translate('xpack.lens.app.goBackLabel', { defaultMessage: `Go back to {contextOriginatingApp}`, values: { contextOriginatingApp }, }), run: actions.goBack, - className: 'lnsNavItem__goBack', + className: 'lnsNavItem__withDivider', testId: 'lnsApp_goBackToAppButton', description: i18n.translate('xpack.lens.app.goBackLabel', { defaultMessage: `Go back to {contextOriginatingApp}`, @@ -116,6 +123,7 @@ function getLensTopNavConfig(options: { label: exploreDataInDiscoverLabel, run: () => {}, testId: 'lnsApp_openInDiscover', + className: 'lnsNavItem__withDivider', description: exploreDataInDiscoverLabel, disableButton: Boolean(tooltips.showUnderlyingDataWarning()), tooltip: tooltips.showUnderlyingDataWarning, @@ -154,6 +162,7 @@ function getLensTopNavConfig(options: { defaultMessage: 'Settings', }), run: actions.openSettings, + className: 'lnsNavItem__withDivider', testId: 'lnsApp_settingsButton', description: i18n.translate('xpack.lens.app.settingsAriaLabel', { defaultMessage: 'Open the Lens settings menu', @@ -175,8 +184,10 @@ function getLensTopNavConfig(options: { topNavMenu.push({ label: saveButtonLabel, - iconType: !showSaveAndReturn ? 'save' : undefined, - emphasize: !showSaveAndReturn, + iconType: (showReplaceInDashboard || showReplaceInCanvas ? false : !showSaveAndReturn) + ? 'save' + : undefined, + emphasize: showReplaceInDashboard || showReplaceInCanvas ? false : !showSaveAndReturn, run: actions.showSaveModal, testId: 'lnsApp_saveButton', description: i18n.translate('xpack.lens.app.saveButtonAriaLabel', { @@ -187,11 +198,15 @@ function getLensTopNavConfig(options: { if (showSaveAndReturn) { topNavMenu.push({ - label: i18n.translate('xpack.lens.app.saveAndReturn', { - defaultMessage: 'Save and return', - }), + label: contextFromEmbeddable + ? i18n.translate('xpack.lens.app.saveAndReplace', { + defaultMessage: 'Save and replace', + }) + : i18n.translate('xpack.lens.app.saveAndReturn', { + defaultMessage: 'Save and return', + }), emphasize: true, - iconType: 'checkInCircleFilled', + iconType: contextFromEmbeddable ? 'save' : 'checkInCircleFilled', run: actions.saveAndReturn, testId: 'lnsApp_saveAndReturnButton', disableButton: !isSaveable, @@ -200,6 +215,40 @@ function getLensTopNavConfig(options: { }), }); } + + if (showReplaceInDashboard) { + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.replaceInDashboard', { + defaultMessage: 'Replace in dashboard', + }), + emphasize: true, + iconType: 'merge', + run: actions.saveAndReturn, + testId: 'lnsApp_replaceInDashboardButton', + disableButton: !isSaveable, + description: i18n.translate('xpack.lens.app.replaceInDashboardButtonAriaLabel', { + defaultMessage: + 'Replace legacy visualization with lens visualization and return to the dashboard', + }), + }); + } + + if (showReplaceInCanvas) { + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.replaceInCanvas', { + defaultMessage: 'Replace in canvas', + }), + emphasize: true, + iconType: 'merge', + run: actions.saveAndReturn, + testId: 'lnsApp_replaceInCanvasButton', + disableButton: !isSaveable, + description: i18n.translate('xpack.lens.app.replaceInCanvasButtonAriaLabel', { + defaultMessage: + 'Replace legacy visualization with lens visualization and return to the canvas', + }), + }); + } return topNavMenu; } @@ -452,13 +501,23 @@ export const LensTopNavMenu = ({ const lensStore = useStore(); const topNavConfig = useMemo(() => { + const showReplaceInDashboard = + initialContext?.originatingApp === 'dashboards' && + !(initialInput as LensByReferenceInput)?.savedObjectId; + const showReplaceInCanvas = + initialContext?.originatingApp === 'canvas' && + !(initialInput as LensByReferenceInput)?.savedObjectId; + const contextFromEmbeddable = + initialContext && 'isEmbeddable' in initialContext && initialContext.isEmbeddable; const baseMenuEntries = getLensTopNavConfig({ showSaveAndReturn: - Boolean( + !(showReplaceInDashboard || showReplaceInCanvas) && + (Boolean( isLinkedToOriginatingApp && // Temporarily required until the 'by value' paradigm is default. (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) - ) || Boolean(initialContextIsEmbedded), + ) || + Boolean(initialContextIsEmbedded)), enableExportToCSV: Boolean(isSaveable && activeData && Object.keys(activeData).length), showOpenInDiscover: Boolean(layerMetaInfo?.isVisible), isByValueMode: getIsByValueMode(), @@ -468,6 +527,9 @@ export const LensTopNavMenu = ({ savingToDashboardPermitted, isSaveable, contextOriginatingApp, + showReplaceInDashboard, + showReplaceInCanvas, + contextFromEmbeddable, tooltips: { showExportWarning: () => { if (activeData) { @@ -527,7 +589,17 @@ export const LensTopNavMenu = ({ }); runSave( { - newTitle: title || '', + newTitle: + title || + (initialContext && 'isEmbeddable' in initialContext && initialContext.isEmbeddable + ? i18n.translate('xpack.lens.app.convertedLabel', { + defaultMessage: '{title} (converted)', + values: { + title: + initialContext.title || `${initialContext.visTypeTitle} visualization`, + }, + }) + : ''), newCopyOnSave: false, isTitleDuplicateConfirmed: false, returnToOrigin: true, @@ -622,6 +694,7 @@ export const LensTopNavMenu = ({ isOnTextBasedMode, lensStore, theme$, + initialContext, ]); const onQuerySubmitWrapped = useCallback( diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 82873673271c6..81cc7df0b005d 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -24,14 +24,14 @@ import { AnalyticsNoDataPage, } from '@kbn/shared-ux-page-analytics-no-data'; -import { ACTION_VISUALIZE_LENS_FIELD } from '@kbn/ui-actions-plugin/public'; +import { ACTION_VISUALIZE_LENS_FIELD, VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { EuiLoadingSpinner } from '@elastic/eui'; import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; import { App } from './app'; -import { EditorFrameStart, LensTopNavMenuEntryGenerator } from '../types'; +import { EditorFrameStart, LensTopNavMenuEntryGenerator, VisualizeEditorContext } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; @@ -56,7 +56,8 @@ import { getLensInspectorService } from '../lens_inspector_service'; export async function getLensServices( coreStart: CoreStart, startDependencies: LensPluginStartDependencies, - attributeService: LensAttributeService + attributeService: LensAttributeService, + initialContext?: VisualizeFieldContext | VisualizeEditorContext ): Promise { const { data, @@ -100,9 +101,9 @@ export async function getLensServices( dashboard: startDependencies.dashboard, charts: startDependencies.charts, getOriginatingAppName: () => { - return embeddableEditorIncomingState?.originatingApp - ? stateTransfer?.getAppNameFromId(embeddableEditorIncomingState.originatingApp) - : undefined; + const originatingApp = + embeddableEditorIncomingState?.originatingApp ?? initialContext?.originatingApp; + return originatingApp ? stateTransfer?.getAppNameFromId(originatingApp) : undefined; }, dataViews: startDependencies.dataViews, // Temporarily required until the 'by value' paradigm is default. @@ -136,7 +137,20 @@ export async function mountApp( ]); const historyLocationState = params.history.location.state as HistoryLocationState; - const lensServices = await getLensServices(coreStart, startDependencies, attributeService); + // get state from location, used for navigating from Visualize/Discover to Lens + const initialContext = + historyLocationState && + (historyLocationState.type === ACTION_VISUALIZE_LENS_FIELD || + historyLocationState.type === ACTION_CONVERT_TO_LENS) + ? historyLocationState.payload + : undefined; + + const lensServices = await getLensServices( + coreStart, + startDependencies, + attributeService, + initialContext + ); const { stateTransfer, data } = lensServices; @@ -206,13 +220,6 @@ export async function mountApp( }); } }; - // get state from location, used for navigating from Visualize/Discover to Lens - const initialContext = - historyLocationState && - (historyLocationState.type === ACTION_VISUALIZE_LENS_FIELD || - historyLocationState.type === ACTION_CONVERT_TO_LENS) - ? historyLocationState.payload - : undefined; if (historyLocationState && historyLocationState.type === ACTION_VISUALIZE_LENS_FIELD) { // remove originatingApp from context when visualizing a field in Lens diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index 2dbc86e380bc9..e45fee5545c4e 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { isFilterPinned } from '@kbn/es-query'; - +import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import type { SavedObjectReference } from '@kbn/core/public'; import { SaveModal } from './save_modal'; import type { LensAppProps, LensAppServices } from './types'; @@ -18,6 +18,7 @@ import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common'; import type { LensAppState } from '../state_management'; import { getPersisted } from '../state_management/init_middleware/load_initial'; +import { VisualizeEditorContext } from '../types'; type ExtraProps = Pick & Partial>; @@ -33,6 +34,7 @@ export type SaveModalContainerProps = { isSaveable?: boolean; getAppNameFromId?: () => string | undefined; lensServices: LensAppServices; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; } & ExtraProps; export function SaveModalContainer({ @@ -49,6 +51,7 @@ export function SaveModalContainer({ isSaveable = true, lastKnownDoc: initLastKnownDoc, lensServices, + initialContext, }: SaveModalContainerProps) { let title = ''; let description; @@ -60,6 +63,20 @@ export function SaveModalContainer({ savedObjectId = lastKnownDoc.savedObjectId; } + if ( + !lastKnownDoc?.title && + initialContext && + 'isEmbeddable' in initialContext && + initialContext.isEmbeddable + ) { + title = i18n.translate('xpack.lens.app.convertedLabel', { + defaultMessage: '{title} (converted)', + values: { + title: initialContext.title || `${initialContext.visTypeTitle} visualization`, + }, + }); + } + const { attributeService, savedObjectsTagging, application, dashboardFeatureFlag } = lensServices; useEffect(() => { diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 04c177237198f..f0c09a9fe31a7 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -24,7 +24,11 @@ import type { ExpressionsSetup, ExpressionsStart, } from '@kbn/expressions-plugin/public'; -import type { VisualizationsSetup, VisualizationsStart } from '@kbn/visualizations-plugin/public'; +import { + DASHBOARD_VISUALIZATION_PANEL_TRIGGER, + VisualizationsSetup, + VisualizationsStart, +} from '@kbn/visualizations-plugin/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import type { UrlForwardingSetup } from '@kbn/url-forwarding-plugin/public'; import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public'; @@ -91,6 +95,7 @@ import { createOpenInDiscoverAction } from './trigger_actions/open_in_discover_a import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; import { visualizeTSVBAction } from './trigger_actions/visualize_tsvb_actions'; import { visualizeAggBasedVisAction } from './trigger_actions/visualize_agg_based_vis_actions'; +import { visualizeDashboardVisualizePanelction } from './trigger_actions/dashboard_visualize_panel_actions'; import type { LensEmbeddableInput } from './embeddable'; import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory'; @@ -507,6 +512,11 @@ export class LensPlugin { visualizeTSVBAction(core.application) ); + startDependencies.uiActions.addTriggerAction( + DASHBOARD_VISUALIZATION_PANEL_TRIGGER, + visualizeDashboardVisualizePanelction(core.application) + ); + startDependencies.uiActions.addTriggerAction( AGG_BASED_VISUALIZATION_TRIGGER, visualizeAggBasedVisAction(core.application) diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 69e93f7a763a3..7ca55e9447392 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { cloneDeep } from 'lodash'; import { MiddlewareAPI } from '@reduxjs/toolkit'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; @@ -125,6 +126,15 @@ export function loadInitial( (attributeService.inputIsRefType(initialInput) && initialInput.savedObjectId === lens.persistedDoc?.savedObjectId) ) { + const newFilters = + initialContext && 'searchFilters' in initialContext && initialContext.searchFilters + ? cloneDeep(initialContext.searchFilters) + : undefined; + + if (newFilters) { + data.query.filterManager.setAppFilters(newFilters); + } + return initializeSources( { datasourceMap, diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index e8874fbcda822..e74e8c94edede 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -78,13 +78,20 @@ export const getPreloadedState = ({ // only if Lens was opened with the intention to visualize a field (e.g. coming from Discover) query: !initialContext ? data.query.queryString.getDefaultQuery() + : 'searchQuery' in initialContext && initialContext.searchQuery + ? initialContext.searchQuery : (data.query.queryString.getQuery() as Query), filters: !initialContext ? data.query.filterManager.getGlobalFilters() + : 'searchFilters' in initialContext && initialContext.searchFilters + ? initialContext.searchFilters : data.query.filterManager.getFilters(), searchSessionId: data.search.session.getSessionId(), resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), - isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp), + isLinkedToOriginatingApp: Boolean( + embeddableEditorIncomingState?.originatingApp || + (initialContext && 'isEmbeddable' in initialContext && initialContext?.isEmbeddable) + ), activeDatasourceId: initialDatasourceId, datasourceStates, visualization: { diff --git a/x-pack/plugins/lens/public/trigger_actions/dashboard_visualize_panel_actions.ts b/x-pack/plugins/lens/public/trigger_actions/dashboard_visualize_panel_actions.ts new file mode 100644 index 0000000000000..943b656aba57e --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/dashboard_visualize_panel_actions.ts @@ -0,0 +1,43 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { createAction } from '@kbn/ui-actions-plugin/public'; +import { + ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS, + ACTION_CONVERT_TO_LENS, +} from '@kbn/visualizations-plugin/public'; +import type { ApplicationStart } from '@kbn/core/public'; +import type { VisualizeEditorContext } from '../types'; + +export const visualizeDashboardVisualizePanelction = (application: ApplicationStart) => + createAction<{ [key: string]: VisualizeEditorContext }>({ + type: ACTION_CONVERT_TO_LENS, + id: ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS, + getDisplayName: () => + i18n.translate('xpack.lens.visualizeLegacyVisualizationChart', { + defaultMessage: 'Visualize legacy visualization chart', + }), + isCompatible: async () => !!application.capabilities.visualize.show, + execute: async (context: { [key: string]: VisualizeEditorContext }) => { + const table = Object.values(context.layers); + const payload = { + ...context, + layers: table, + isVisualizeAction: true, + }; + application.navigateToApp('lens', { + state: { + type: ACTION_CONVERT_TO_LENS, + payload, + originatingApp: i18n.translate('xpack.lens.dashboardLabel', { + defaultMessage: 'Dashboard', + }), + }, + }); + }, + }); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index f2bb4226a2085..4cc0b4c4948fa 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -248,6 +248,11 @@ export type VisualizeEditorContext = { vizEditorOriginatingAppUrl?: string; originatingApp?: string; isVisualizeAction: boolean; + searchQuery?: Query; + searchFilters?: Filter[]; + title?: string; + visTypeTitle?: string; + isEmbeddable?: boolean; } & NavigateToLensContext; export interface GetDropPropsArgs { diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 299c1340e8599..dfe479f0d67cb 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -65,6 +65,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ withKibana: (comp) => { return comp; }, + reactToUiComponent: jest.fn(), })); import { shallowWithIntl, mountWithIntl } from '@kbn/test-jest-helpers'; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index bbfdf7ef6b0e4..f8e08183fd914 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -54,6 +54,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ withKibana: (node) => { return node; }, + reactToUiComponent: jest.fn(), })); const testingState = { diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/header.test.js b/x-pack/plugins/ml/public/application/settings/calendars/list/header.test.js index ba4d49a31135c..31a8b9760482d 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/header.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/header.test.js @@ -14,6 +14,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ withKibana: (comp) => { return comp; }, + reactToUiComponent: jest.fn(), })); describe('CalendarListsHeader', () => { diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js index 45c43e04daa68..f30a25bbcb493 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js @@ -49,6 +49,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ withKibana: (node) => { return node; }, + reactToUiComponent: jest.fn(), })); import { shallowWithIntl } from '@kbn/test-jest-helpers'; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index 5d0306564e312..b3cd815673aad 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -30,6 +30,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({ withKibana: (node) => { return node; }, + reactToUiComponent: jest.fn(), })); // Mock the call for loading the list of filters. diff --git a/x-pack/test/accessibility/apps/dashboard_panel_options.ts b/x-pack/test/accessibility/apps/dashboard_panel_options.ts index 9cd49623cfae5..b56025291cf9b 100644 --- a/x-pack/test/accessibility/apps/dashboard_panel_options.ts +++ b/x-pack/test/accessibility/apps/dashboard_panel_options.ts @@ -112,7 +112,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('dashboard panel - edit panel title', async () => { await dashboardPanelActions.toggleContextMenu(header); - await testSubjects.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); + await dashboardPanelActions.customizePanel(); await a11y.testAppSnapshot(); await testSubjects.click('customizePanelHideTitle'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/dashboard/config.ts b/x-pack/test/functional/apps/lens/open_in_lens/dashboard/config.ts new file mode 100644 index 0000000000000..3bf1f38d29ca9 --- /dev/null +++ b/x-pack/test/functional/apps/lens/open_in_lens/dashboard/config.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/lens/open_in_lens/dashboard/dashboard.ts b/x-pack/test/functional/apps/lens/open_in_lens/dashboard/dashboard.ts new file mode 100644 index 0000000000000..b6c94a4f32969 --- /dev/null +++ b/x-pack/test/functional/apps/lens/open_in_lens/dashboard/dashboard.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { lens, dashboard, canvas } = getPageObjects(['lens', 'dashboard', 'canvas']); + + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const panelActions = getService('dashboardPanelActions'); + const dashboardAddPanel = getService('dashboardAddPanel'); + + describe('Convert to Lens action on dashboard', function describeIndexTests() { + before(async () => { + await dashboard.initTests(); + }); + + it('should show notification in context menu if visualization can be converted', async () => { + await dashboard.clickNewDashboard(); + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await dashboardAddPanel.clickVisType('area'); + await testSubjects.click('savedObjectTitlelogstash-*'); + await testSubjects.exists('visualizesaveAndReturnButton'); + await testSubjects.click('visualizesaveAndReturnButton'); + await dashboard.waitForRenderComplete(); + expect(await dashboard.isNotificationExists(0)).to.be(true); + }); + + it('should convert legacy visualization to lens by clicking "convert to lens" action', async () => { + const originalEmbeddableCount = await canvas.getEmbeddableCount(); + await panelActions.convertToLens(); + await lens.waitForVisualization('xyVisChart'); + const lastBreadcrumbdcrumb = await testSubjects.getVisibleText('breadcrumb last'); + expect(lastBreadcrumbdcrumb).to.be('Converting Area visualization'); + await lens.replaceInDashboard(); + + await retry.try(async () => { + const embeddableCount = await canvas.getEmbeddableCount(); + expect(embeddableCount).to.eql(originalEmbeddableCount); + }); + + const titles = await dashboard.getPanelTitles(); + + expect(titles[0]).to.be('Area visualization (converted)'); + + expect(await dashboard.isNotificationExists(0)).to.be(false); + }); + + it('should not show notification in context menu if visualization can not be converted', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAggBasedVisualizations(); + await dashboardAddPanel.clickVisType('timelion'); + await testSubjects.exists('visualizesaveAndReturnButton'); + await testSubjects.click('visualizesaveAndReturnButton'); + await dashboard.waitForRenderComplete(); + expect(await dashboard.isNotificationExists(1)).to.be(false); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/open_in_lens/dashboard/index.ts b/x-pack/test/functional/apps/lens/open_in_lens/dashboard/index.ts new file mode 100644 index 0000000000000..3d9e8e53c6e4e --- /dev/null +++ b/x-pack/test/functional/apps/lens/open_in_lens/dashboard/index.ts @@ -0,0 +1,74 @@ +/* + * 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 { EsArchiver } from '@kbn/es-archiver'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile, getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker']); + const config = getService('config'); + let remoteEsArchiver; + + describe('lens app - TSVB Open in Lens', () => { + const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; + const localIndexPatternString = 'logstash-*'; + const remoteIndexPatternString = 'ftr-remote:logstash-*'; + const localFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/default', + }; + + const remoteFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/default', + }; + let esNode: EsArchiver; + let fixtureDirs: { + lensBasic: string; + lensDefault: string; + }; + let indexPatternString: string; + before(async () => { + log.debug('Starting lens before method'); + await browser.setWindowSize(1280, 1200); + try { + config.get('esTestCluster.ccs'); + remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); + esNode = remoteEsArchiver; + fixtureDirs = remoteFixtures; + indexPatternString = remoteIndexPatternString; + } catch (error) { + esNode = esArchiver; + fixtureDirs = localFixtures; + indexPatternString = localIndexPatternString; + } + + await esNode.load(esArchive); + // changing the timepicker default here saves us from having to set it in Discover (~8s) + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update({ + defaultIndex: indexPatternString, + 'dateFormat:tz': 'UTC', + }); + await kibanaServer.importExport.load(fixtureDirs.lensBasic); + await kibanaServer.importExport.load(fixtureDirs.lensDefault); + }); + + after(async () => { + await esArchiver.unload(esArchive); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.importExport.unload(fixtureDirs.lensBasic); + await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + }); + + loadTestFile(require.resolve('./dashboard')); + }); +} diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts index 72daa5ff5486b..998d2df927cf2 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts @@ -52,7 +52,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await dimensions[1].getVisibleText()).to.be('Count of records'); }); - await lens.saveAndReturn(); + await lens.replaceInDashboard(); await retry.try(async () => { const embeddableCount = await canvas.getEmbeddableCount(); expect(embeddableCount).to.eql(originalEmbeddableCount); @@ -80,7 +80,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await dimensions[1].getVisibleText()).to.be('Count of records'); }); - await lens.saveAndReturn(); + await lens.replaceInDashboard(); await retry.try(async () => { const embeddableCount = await canvas.getEmbeddableCount(); expect(embeddableCount).to.eql(originalEmbeddableCount); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 495135c1ece5c..a2d0faa34dc99 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -712,6 +712,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lnsApp_saveAndReturnButton'); }, + async replaceInDashboard() { + await testSubjects.click('lnsApp_replaceInDashboardButton'); + }, + async expectSaveAndReturnButtonDisabled() { const button = await testSubjects.find('lnsApp_saveAndReturnButton', 10000); const disabledAttr = await button.getAttribute('disabled');