mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
0aa6e1c398
commit
a068b2ec53
42 changed files with 694 additions and 52 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
"presentationUtil",
|
||||
"dataViews",
|
||||
"dataViewEditor",
|
||||
"unifiedSearch"
|
||||
"unifiedSearch",
|
||||
"usageCollection"
|
||||
],
|
||||
"optionalPlugins": ["home", "share", "spaces", "savedObjectsTaggingOss"],
|
||||
"requiredBundles": ["kibanaUtils", "savedSearch", "kibanaReact", "charts"],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -77,6 +77,9 @@ const createInstance = async () => {
|
|||
screenshotMode: screenshotModePluginMock.createStartContract(),
|
||||
fieldFormats: fieldFormatsServiceMock.createStartContract(),
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
usageCollection: {
|
||||
reportUiCounter: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -39,6 +39,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
|||
},
|
||||
})),
|
||||
withKibana: jest.fn((comp) => comp),
|
||||
reactToUiComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../services', () => ({
|
||||
|
|
|
@ -310,7 +310,6 @@ export const getTopNavConfig = (
|
|||
);
|
||||
const updatedWithMeta = {
|
||||
...navigateToLensConfig,
|
||||
savedObjectId: visInstance.vis.id,
|
||||
embeddableId,
|
||||
vizEditorOriginatingAppUrl: getVizEditorOriginatingAppUrl(history),
|
||||
originatingApp,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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> {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -54,6 +54,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
|||
withKibana: (node) => {
|
||||
return node;
|
||||
},
|
||||
reactToUiComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
const testingState = {
|
||||
|
|
|
@ -14,6 +14,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
|||
withKibana: (comp) => {
|
||||
return comp;
|
||||
},
|
||||
reactToUiComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CalendarListsHeader', () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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('.')],
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue