[Dashboard][Lens] Add "convert to lens" action to dashboard (#146363)

## Summary

Closes https://github.com/elastic/kibana/issues/147032
Completes part of: https://github.com/elastic/kibana/issues/144605

Added `convert to lens` action for panel in dashboards.

If legacy visualization can be converted, the notification 'dot' will
shown on context menu.
<img width="828" alt="Снимок экрана 2022-12-02 в 10 50 58"
src="https://user-images.githubusercontent.com/16915480/205253599-3f3f102e-8fdc-497c-9e81-a9e1a146687c.png">

New action looks like this:
<img width="781" alt="Снимок экрана 2022-12-02 в 10 52 42"
src="https://user-images.githubusercontent.com/16915480/205253909-79d65fd8-81d8-4cce-a61a-234d3996cf84.png">

After clicking by that action user will be navigate to lens page and see
the following, where user can replace legacy visualization to lens on
dashboard:
<img width="1347" alt="Снимок экрана 2022-12-02 в 10 53 23"
src="https://user-images.githubusercontent.com/16915480/205254013-6e26b54d-6b92-4da5-be64-01b2876ea847.png">

On save user also can replace panel on dashboard:
<img width="506" alt="Снимок экрана 2022-12-02 в 10 55 22"
src="https://user-images.githubusercontent.com/16915480/205254409-163ebf51-c075-4c9a-a070-cebc7001636d.png">

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Uladzislau Lasitsa 2022-12-13 15:36:38 +02:00 committed by GitHub
parent 0aa6e1c398
commit a068b2ec53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 694 additions and 52 deletions

View file

@ -186,6 +186,7 @@ enabled:
- x-pack/test/functional/apps/lens/group3/config.ts
- x-pack/test/functional/apps/lens/open_in_lens/tsvb/config.ts
- x-pack/test/functional/apps/lens/open_in_lens/agg_based/config.ts
- x-pack/test/functional/apps/lens/open_in_lens/dashboard/config.ts
- x-pack/test/functional/apps/license_management/config.ts
- x-pack/test/functional/apps/logstash/config.ts
- x-pack/test/functional/apps/management/config.ts

View file

@ -109,9 +109,14 @@ export const buildDashboardContainer = async ({
gridData: originalPanelState.gridData,
type: incomingEmbeddable.type,
explicitInput: {
...(incomingEmbeddable.type === originalPanelState.type && {
...originalPanelState.explicitInput,
}),
// even when we change embeddable type we should keep hidePanelTitles state
// this is temporary, and only required because the key is stored in explicitInput
// when it should be stored outside of it instead.
...(incomingEmbeddable.type === originalPanelState.type
? {
...originalPanelState.explicitInput,
}
: { hidePanelTitles: originalPanelState.explicitInput.hidePanelTitles }),
...incomingEmbeddable.input,
id: incomingEmbeddable.embeddableId,
},

View file

@ -106,6 +106,17 @@
&:focus {
background-color: $euiFocusBackgroundColor;
}
}
.embPanel__optionsMenuPopover-notification::after {
position: absolute;
top: 0;
right: 0;
content: '';
transform: translate(50%, -50%);
color: $euiColorAccent;
font-size: $euiSizeL;
}
.embPanel .embPanel__optionsMenuButton {

View file

@ -455,7 +455,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
sortedActions = sortedActions.filter(({ id }) => this.props.actionPredicate!(id));
}
return await buildContextMenuForActions({
const panels = await buildContextMenuForActions({
actions: sortedActions.map((action) => ({
action,
context: { embeddable: this.props.embeddable },
@ -463,5 +463,10 @@ export class EmbeddablePanel extends React.Component<Props, State> {
})),
closeMenu: this.closeMyContextMenuPanel,
});
return {
panels,
actions: sortedActions,
};
};
}

View file

@ -30,7 +30,10 @@ export interface PanelHeaderProps {
index?: number;
isViewMode: boolean;
hidePanelTitle: boolean;
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor[]>;
getActionContextMenuPanel: () => Promise<{
panels: EuiContextMenuPanelDescriptor[];
actions: Action[];
}>;
closeContextMenu: boolean;
badges: Array<Action<EmbeddableContext>>;
notifications: Array<Action<EmbeddableContext>>;

View file

@ -15,9 +15,13 @@ import {
EuiContextMenuPanelDescriptor,
EuiPopover,
} from '@elastic/eui';
import { Action } from '@kbn/ui-actions-plugin/public';
export interface PanelOptionsMenuProps {
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor[]>;
getActionContextMenuPanel: () => Promise<{
panels: EuiContextMenuPanelDescriptor[];
actions: Action[];
}>;
isViewMode: boolean;
closeContextMenu: boolean;
title?: string;
@ -25,8 +29,12 @@ export interface PanelOptionsMenuProps {
}
interface State {
actionContextMenuPanel?: EuiContextMenuPanelDescriptor[];
actionContextMenuPanel?: {
panels: EuiContextMenuPanelDescriptor[];
actions: Action[];
};
isPopoverOpen: boolean;
showNotification: boolean;
}
export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, State> {
@ -47,6 +55,7 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
this.state = {
actionContextMenuPanel: undefined,
isPopoverOpen: false,
showNotification: false,
};
}
@ -54,8 +63,21 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
this.mounted = true;
this.setState({ actionContextMenuPanel: undefined });
const actionContextMenuPanel = await this.props.getActionContextMenuPanel();
const showNotification = actionContextMenuPanel.actions.some(
(action) => action.showNotification
);
if (this.mounted) {
this.setState({ actionContextMenuPanel });
this.setState({ actionContextMenuPanel, showNotification });
}
}
public async componentDidUpdate() {
const actionContextMenuPanel = await this.props.getActionContextMenuPanel();
const showNotification = actionContextMenuPanel.actions.some(
(action) => action.showNotification
);
if (this.mounted && this.state.showNotification !== showNotification) {
this.setState({ showNotification });
}
}
@ -95,7 +117,10 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
return (
<EuiPopover
className="embPanel__optionsMenuPopover"
className={
'embPanel__optionsMenuPopover' +
(this.state.showNotification ? ' embPanel__optionsMenuPopover-notification' : '')
}
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
@ -109,7 +134,7 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
>
<EuiContextMenu
initialPanelId="mainMenu"
panels={this.state.actionContextMenuPanel || []}
panels={this.state.actionContextMenuPanel?.panels || []}
/>
</EuiPopover>
);

View file

@ -104,6 +104,12 @@ export interface Action<Context extends object = object>
*
*/
disabled?: boolean;
/**
* Determines if notification should be shown in menu for that action
*
*/
showNotification?: boolean;
}
/**
@ -151,6 +157,12 @@ export interface ActionDefinition<Context extends object = object>
*
*/
disabled?: boolean;
/**
* Determines if notification should be shown in menu for that action
*
*/
showNotification?: boolean;
}
export type ActionContext<A> = A extends ActionDefinition<infer Context> ? Context : never;

View file

@ -25,6 +25,7 @@ export class ActionInternal<A extends ActionDefinition = ActionDefinition>
public readonly MenuItem?: UiComponent<ActionMenuItemProps<Context<A>>>;
public readonly ReactMenuItem?: React.FC<ActionMenuItemProps<Context<A>>>;
public readonly grouping?: PresentableGrouping<Context<A>>;
public readonly showNotification?: boolean;
constructor(public readonly definition: A) {
this.id = this.definition.id;
@ -33,6 +34,7 @@ export class ActionInternal<A extends ActionDefinition = ActionDefinition>
this.MenuItem = this.definition.MenuItem;
this.ReactMenuItem = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined;
this.grouping = this.definition.grouping;
this.showNotification = this.definition.showNotification;
}
public execute(context: Context<A>) {

View file

@ -18,7 +18,8 @@
"presentationUtil",
"dataViews",
"dataViewEditor",
"unifiedSearch"
"unifiedSearch",
"usageCollection"
],
"optionalPlugins": ["home", "share", "spaces", "savedObjectsTaggingOss"],
"requiredBundles": ["kibanaUtils", "savedSearch", "kibanaReact", "charts"],

View file

@ -0,0 +1,129 @@
/*
* 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.
*/
import React from 'react';
import { take } from 'rxjs/operators';
import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { reactToUiComponent } from '@kbn/kibana-react-plugin/public';
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import { TimefilterContract } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { IEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
import { Action } from '@kbn/ui-actions-plugin/public';
import { VisualizeEmbeddable } from '../embeddable';
import { DASHBOARD_VISUALIZATION_PANEL_TRIGGER } from '../triggers';
import { getUiActions, getApplication, getEmbeddable, getUsageCollection } from '../services';
export const ACTION_EDIT_IN_LENS = 'ACTION_EDIT_IN_LENS';
export interface EditInLensContext {
embeddable: IEmbeddable;
}
const displayName = i18n.translate('visualizations.actions.editInLens.displayName', {
defaultMessage: 'Convert to Lens',
});
const ReactMenuItem: React.FC = () => {
return (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>{displayName}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge color={'accent'}>
{i18n.translate('visualizations.tonNavMenu.tryItBadgeText', {
defaultMessage: 'Try it',
})}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const UiMenuItem = reactToUiComponent(ReactMenuItem);
const isVisualizeEmbeddable = (embeddable: IEmbeddable): embeddable is VisualizeEmbeddable => {
return 'getVis' in embeddable;
};
export class EditInLensAction implements Action<EditInLensContext> {
public id = ACTION_EDIT_IN_LENS;
public readonly type = ACTION_EDIT_IN_LENS;
public order = 49;
public showNotification = true;
public currentAppId: string | undefined;
constructor(private readonly timefilter: TimefilterContract) {}
async execute(context: ActionExecutionContext<EditInLensContext>): Promise<void> {
const application = getApplication();
if (application?.currentAppId$) {
application.currentAppId$
.pipe(take(1))
.subscribe((appId: string | undefined) => (this.currentAppId = appId));
application.currentAppId$.subscribe(() => {
getEmbeddable().getStateTransfer().isTransferInProgress = false;
});
}
const { embeddable } = context;
if (isVisualizeEmbeddable(embeddable)) {
const vis = embeddable.getVis();
const navigateToLensConfig = await vis.type.navigateToLens?.(vis, this.timefilter);
const parentSearchSource = vis.data.searchSource?.getParent();
const searchFilters = parentSearchSource?.getField('filter');
const searchQuery = parentSearchSource?.getField('query');
const title = vis.title || embeddable.getOutput().title;
const updatedWithMeta = {
...navigateToLensConfig,
title,
visTypeTitle: vis.type.title,
embeddableId: embeddable.id,
originatingApp: this.currentAppId,
searchFilters,
searchQuery,
isEmbeddable: true,
};
if (navigateToLensConfig) {
if (this.currentAppId) {
getUsageCollection().reportUiCounter(
this.currentAppId,
METRIC_TYPE.CLICK,
ACTION_EDIT_IN_LENS
);
}
getEmbeddable().getStateTransfer().isTransferInProgress = true;
getUiActions().getTrigger(DASHBOARD_VISUALIZATION_PANEL_TRIGGER).exec(updatedWithMeta);
}
}
}
getDisplayName(context: ActionExecutionContext<EditInLensContext>): string {
return displayName;
}
MenuItem = UiMenuItem;
getIconType(context: ActionExecutionContext<EditInLensContext>): string | undefined {
return 'merge';
}
async isCompatible(context: ActionExecutionContext<EditInLensContext>) {
const { embeddable } = context;
if (!isVisualizeEmbeddable(embeddable)) {
return false;
}
const vis = embeddable.getVis();
if (!vis) {
return false;
}
const canNavigateToLens =
embeddable.getExpressionVariables?.()?.canNavigateToLens ??
(await vis.type.navigateToLens?.(vis, this.timefilter));
return Boolean(canNavigateToLens && embeddable.getInput().viewMode === ViewMode.EDIT);
}
}

View file

@ -126,6 +126,7 @@ export class VisualizeEmbeddable
VisualizeByValueInput,
VisualizeByReferenceInput
>;
private expressionVariables: Record<string, unknown> | undefined;
private readonly expressionVariablesSubject = new ReplaySubject<
Record<string, unknown> | undefined
>(1);
@ -584,12 +585,12 @@ export class VisualizeEmbeddable
private async updateHandler() {
const context = this.getExecutionContext();
const expressionVariables = await this.vis.type.getExpressionVariables?.(
this.expressionVariables = await this.vis.type.getExpressionVariables?.(
this.vis,
this.timefilter
);
this.expressionVariablesSubject.next(expressionVariables);
this.expressionVariablesSubject.next(this.expressionVariables);
const expressionParams: IExpressionLoaderParams = {
searchContext: {
@ -600,7 +601,7 @@ export class VisualizeEmbeddable
},
variables: {
embeddableTitle: this.getTitle(),
...expressionVariables,
...this.expressionVariables,
},
searchSessionId: this.input.searchSessionId,
syncColors: this.input.syncColors,
@ -651,6 +652,10 @@ export class VisualizeEmbeddable
return this.expressionVariablesSubject.asObservable();
}
public getExpressionVariables() {
return this.expressionVariables;
}
inputIsRefType = (input: VisualizeInput): input is VisualizeByReferenceInput => {
if (!this.attributeService) {
throw new Error('AttributeService must be defined for getInputAsRefType');

View file

@ -69,8 +69,10 @@ export type { IEditorController, EditorRenderProps } from './visualize_app/types
export {
VISUALIZE_EDITOR_TRIGGER,
AGG_BASED_VISUALIZATION_TRIGGER,
DASHBOARD_VISUALIZATION_PANEL_TRIGGER,
ACTION_CONVERT_TO_LENS,
ACTION_CONVERT_AGG_BASED_TO_LENS,
ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS,
} from './triggers';
export const convertToLensModule = import('./convert_to_lens');

View file

@ -77,6 +77,9 @@ const createInstance = async () => {
screenshotMode: screenshotModePluginMock.createStartContract(),
fieldFormats: fieldFormatsServiceMock.createStartContract(),
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
usageCollection: {
reportUiCounter: jest.fn(),
},
});
return {

View file

@ -42,6 +42,7 @@ import type {
Setup as InspectorSetup,
Start as InspectorStart,
} from '@kbn/inspector-plugin/public';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public';
@ -57,7 +58,11 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { TypesSetup, TypesStart } from './vis_types';
import type { VisualizeServices } from './visualize_app/types';
import { aggBasedVisualizationTrigger, visualizeEditorTrigger } from './triggers';
import {
aggBasedVisualizationTrigger,
dashboardVisualizationPanelTrigger,
visualizeEditorTrigger,
} from './triggers';
import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry';
import { showNewVisModal } from './wizard';
import { VisualizeLocatorDefinition } from '../common/locator';
@ -91,8 +96,10 @@ import {
setExecutionContext,
setFieldFormats,
setSavedObjectTagging,
setUsageCollection,
} from './services';
import { VisualizeConstants } from '../common/constants';
import { EditInLensAction } from './actions/edit_in_lens_action';
/**
* Interface for this plugin's returned setup/start contracts.
@ -138,6 +145,7 @@ export interface VisualizationsStartDeps {
screenshotMode: ScreenshotModePluginStart;
fieldFormats: FieldFormatsStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
usageCollection: UsageCollectionStart;
}
/**
@ -340,6 +348,9 @@ export class VisualizationsPlugin
expressions.registerFunction(xyDimensionExpressionFunction);
uiActions.registerTrigger(aggBasedVisualizationTrigger);
uiActions.registerTrigger(visualizeEditorTrigger);
uiActions.registerTrigger(dashboardVisualizationPanelTrigger);
const editInLensAction = new EditInLensAction(data.query.timefilter.timefilter);
uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', editInLensAction);
const embeddableFactory = new VisualizeEmbeddableFactory({ start });
embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory);
@ -360,6 +371,7 @@ export class VisualizationsPlugin
spaces,
savedObjectsTaggingOss,
fieldFormats,
usageCollection,
}: VisualizationsStartDeps
): VisualizationsStart {
const types = this.types.start();
@ -379,6 +391,7 @@ export class VisualizationsPlugin
setExecutionContext(core.executionContext);
setChrome(core.chrome);
setFieldFormats(fieldFormats);
setUsageCollection(usageCollection);
if (spaces) {
setSpaces(spaces);

View file

@ -26,6 +26,7 @@ import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { TypesStart } from './vis_types';
export const [getUISettings, setUISettings] = createGetterSetter<IUiSettingsClient>('UISettings');
@ -72,3 +73,6 @@ export const [getSpaces, setSpaces] = createGetterSetter<SpacesPluginStart>('Spa
export const [getSavedObjectTagging, setSavedObjectTagging] =
createGetterSetter<SavedObjectTaggingOssPluginStart>('SavedObjectTagging', false);
export const [getUsageCollection, setUsageCollection] =
createGetterSetter<UsageCollectionStart>('UsageCollection');

View file

@ -22,5 +22,13 @@ export const aggBasedVisualizationTrigger: Trigger = {
description: 'Triggered when user navigates from a agg based visualization to Lens.',
};
export const DASHBOARD_VISUALIZATION_PANEL_TRIGGER = 'DASHBOARD_VISUALIZATION_PANEL_TRIGGER';
export const dashboardVisualizationPanelTrigger: Trigger = {
id: DASHBOARD_VISUALIZATION_PANEL_TRIGGER,
title: 'Convert legacy visualization panel on dashboard to Lens',
description: 'Triggered when user use "Edit in Lens" action on dashboard panel',
};
export const ACTION_CONVERT_TO_LENS = 'ACTION_CONVERT_TO_LENS';
export const ACTION_CONVERT_AGG_BASED_TO_LENS = 'ACTION_CONVERT_AGG_BASED_TO_LENS';
export const ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS = 'ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS';

View file

@ -39,6 +39,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
},
})),
withKibana: jest.fn((comp) => comp),
reactToUiComponent: jest.fn(),
}));
jest.mock('../../services', () => ({

View file

@ -310,7 +310,6 @@ export const getTopNavConfig = (
);
const updatedWithMeta = {
...navigateToLensConfig,
savedObjectId: visInstance.vis.id,
embeddableId,
vizEditorOriginatingAppUrl: getVizEditorOriginatingAppUrl(history),
originatingApp,

View file

@ -789,4 +789,15 @@ export class DashboardPageObject extends FtrService {
public async getPanelChartDebugState(panelIndex: number) {
return await this.elasticChart.getChartDebugData(undefined, panelIndex);
}
public async isNotificationExists(panelIndex = 0) {
const panel = (await this.getDashboardPanels())[panelIndex];
try {
const notification = await panel.findByClassName('embPanel__optionsMenuPopover-notification');
return Boolean(notification);
} catch (e) {
// if not found then this is false
return false;
}
}
}

View file

@ -20,6 +20,7 @@ const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector';
const COPY_PANEL_TO_DATA_TEST_SUBJ = 'embeddablePanelAction-copyToDashboard';
const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary';
const UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-unlinkFromLibrary';
const CONVERT_TO_LENS_TEST_SUBJ = 'embeddablePanelAction-ACTION_EDIT_IN_LENS';
export class DashboardPanelActionsService extends FtrService {
private readonly log = this.ctx.getService('log');
@ -120,7 +121,12 @@ export class DashboardPanelActionsService extends FtrService {
}
async customizePanel(parent?: WebElementWrapper) {
this.log.debug('customizePanel');
await this.openContextMenu(parent);
const isActionVisible = await this.testSubjects.exists(CUSTOMIZE_PANEL_DATA_TEST_SUBJ);
if (!isActionVisible) await this.clickContextMenuMoreItem();
const isPanelActionVisible = await this.testSubjects.exists(CUSTOMIZE_PANEL_DATA_TEST_SUBJ);
if (!isPanelActionVisible) await this.clickContextMenuMoreItem();
await this.testSubjects.click(CUSTOMIZE_PANEL_DATA_TEST_SUBJ);
}
@ -352,4 +358,14 @@ export class DashboardPanelActionsService extends FtrService {
throw new Error(`No action matching text "${text}"`);
}
async convertToLens(parent?: WebElementWrapper) {
this.log.debug('convertToLens');
await this.openContextMenu(parent);
const isActionVisible = await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ);
if (!isActionVisible) await this.clickContextMenuMoreItem();
const isPanelActionVisible = await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ);
if (!isPanelActionVisible) await this.clickContextMenuMoreItem();
await this.testSubjects.click(CONVERT_TO_LENS_TEST_SUBJ);
}
}

View file

@ -53,11 +53,20 @@ export const useIncomingEmbeddable = (selectedPage: CanvasPage) => {
originalAst.chain[functionIndex].arguments.config[0] as string
);
const originalType = originalAst.chain[functionIndex].arguments.type[0];
// clear out resolved arg for old embeddable
const argumentPath = [embeddableId, 'expressionRenderable'];
dispatch(clearValue({ path: argumentPath }));
const updatedInput = { ...originalInput, ...incomingInput };
let updatedInput;
// if type was changed, we should not provide originalInput
if (originalType !== type) {
updatedInput = incomingInput;
} else {
updatedInput = { ...originalInput, ...incomingInput };
}
const expression = `embeddable config="${encode(updatedInput)}"
type="${type}"

View file

@ -24,7 +24,7 @@
}
// Less-than-ideal styles to add a vertical divider after this button. Consider restructuring markup for better semantics and styling options in the future.
.lnsNavItem__goBack {
.lnsNavItem__withDivider {
@include euiBreakpoint('m', 'l', 'xl') {
margin-right: $euiSizeM;
position: relative;

View file

@ -250,6 +250,19 @@ export function App({
? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' })
: persistedDoc.title;
}
if (
!persistedDoc?.title &&
initialContext &&
'isEmbeddable' in initialContext &&
initialContext.isEmbeddable
) {
currentDocTitle = i18n.translate('xpack.lens.breadcrumbsEditInLensFromDashboard', {
defaultMessage: 'Converting {title} visualization',
values: {
title: initialContext.title ? `"${initialContext.title}"` : initialContext.visTypeTitle,
},
});
}
breadcrumbs.push({ text: currentDocTitle });
chrome.setBreadcrumbs(breadcrumbs);
}, [
@ -414,6 +427,19 @@ export function App({
};
}, []);
const returnToOriginSwitchLabelForContext =
initialContext &&
'isEmbeddable' in initialContext &&
initialContext.isEmbeddable &&
!persistedDoc
? i18n.translate('xpack.lens.app.replacePanel', {
defaultMessage: 'Replace panel on {originatingApp}',
values: {
originatingApp: initialContext?.originatingApp,
},
})
: undefined;
return (
<>
<div className="lnsApp" data-test-subj="lnsApp" role="main">
@ -453,7 +479,11 @@ export function App({
{isSaveModalVisible && (
<SaveModalContainer
lensServices={lensAppServices}
originatingApp={isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined}
originatingApp={
isLinkedToOriginatingApp
? incomingState?.originatingApp ?? initialContext?.originatingApp
: undefined
}
isSaveable={isSaveable}
runSave={runSave}
onClose={() => {
@ -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)
}
/>
)}

View file

@ -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(

View file

@ -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<LensAppServices> {
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

View file

@ -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<LensAppProps, 'initialInput'> &
Partial<Pick<LensAppProps, 'redirectToOrigin' | 'redirectTo' | 'onAppLeave'>>;
@ -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(() => {

View file

@ -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)

View file

@ -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,

View file

@ -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: {

View file

@ -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',
}),
},
});
},
});

View file

@ -248,6 +248,11 @@ export type VisualizeEditorContext<T extends Configuration = Configuration> = {
vizEditorOriginatingAppUrl?: string;
originatingApp?: string;
isVisualizeAction: boolean;
searchQuery?: Query;
searchFilters?: Filter[];
title?: string;
visTypeTitle?: string;
isEmbeddable?: boolean;
} & NavigateToLensContext<T>;
export interface GetDropPropsArgs<T = unknown> {

View file

@ -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';

View file

@ -54,6 +54,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
withKibana: (node) => {
return node;
},
reactToUiComponent: jest.fn(),
}));
const testingState = {

View file

@ -14,6 +14,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
withKibana: (comp) => {
return comp;
},
reactToUiComponent: jest.fn(),
}));
describe('CalendarListsHeader', () => {

View file

@ -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';

View file

@ -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.

View file

@ -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();

View file

@ -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('.')],
};
}

View file

@ -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);
});
});
}

View file

@ -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'));
});
}

View file

@ -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);

View file

@ -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');