Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 24 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7df2590
Add "convert to lens" action to dashboard
VladLasitsa Nov 28, 2022
1b00b5e
Fix CI
VladLasitsa Nov 28, 2022
0fca07e
Fix more tests
VladLasitsa Nov 28, 2022
f78d79b
Fix test
VladLasitsa Nov 29, 2022
8676538
Some improvments
VladLasitsa Nov 29, 2022
fe7c5cd
Add functional tests
VladLasitsa Dec 1, 2022
efe7d80
Merge branch 'main' into convert_to_lens_for_dashboard
kibanamachine Dec 2, 2022
a2397db
Merge branch 'main' into convert_to_lens_for_dashboard
stratoula Dec 5, 2022
15d3380
Fix notification
VladLasitsa Dec 5, 2022
2e22b1e
Fix some comments
VladLasitsa Dec 5, 2022
c7fdc8e
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Dec 5, 2022
b6f99e8
Fix some more nits
VladLasitsa Dec 6, 2022
8a74239
Merge remote-tracking branch 'origin/convert_to_lens_for_dashboard' i…
VladLasitsa Dec 6, 2022
0a6c890
Fix some tests
VladLasitsa Dec 6, 2022
cf4ac72
Merge branch 'main' into convert_to_lens_for_dashboard
kibanamachine Dec 7, 2022
91020f0
Fix some new comments
VladLasitsa Dec 7, 2022
54161f8
Some fixes
VladLasitsa Dec 8, 2022
a5447b4
Fix mocks
VladLasitsa Dec 8, 2022
135b36d
Merge branch 'main' into convert_to_lens_for_dashboard
kibanamachine Dec 8, 2022
1489189
Merge branch 'main' into convert_to_lens_for_dashboard
kibanamachine Dec 12, 2022
9b35aa5
Fix small nits
VladLasitsa Dec 13, 2022
ed576bd
Merge remote-tracking branch 'upstream/main' into convert_to_lens_for…
VladLasitsa Dec 13, 2022
1d260b1
fix test
VladLasitsa Dec 13, 2022
705101f
Get correct title for replace action
VladLasitsa Dec 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .buildkite/ftr_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,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
Expand Down
11 changes: 11 additions & 0 deletions src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@
&:focus {
background-color: $euiFocusBackgroundColor;
}

}

.embPanel__optionsMenuPopover-notification::after {
position: absolute;
top: 0;
right: 0;
content: '•';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use an EuiIcon component with the dot glyph, rather than a pseudo element with a bullet symbol?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelMarcialis, Adding EuiIcon anyway will require styles for correct position. For me in that case using pseudo element is good solution. Could you please describe a reason why is better adding new element instead of using pseudo element?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason I asked was for the sake of consistency with other areas of Kibana that use the dot icon. That consistency would really only be in regard to size in this case, since the icon is a simple circle. That said, it's not a huge deal either way. If positioning an EuiIcon instead of a pseudo element is more complex, I'm OK keeping as you have it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelMarcialis, Could you please tell me where we have the same dot-notification idea in kibana?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is the first use of a dot icon to draw attention to a menu button in Kibana, but we do utilize the dot icon in a variety of other across Kibana as a means to visually offset or grab attention from users. Some that immediately jump to mind are 1) for indicating change/dirty state when making edits on some pages (like user profiles) and 2) for indicating incompatibility of quick functions in Lens.

transform: translate(50%, -50%);
color: $euiColorAccent;
font-size: $euiSizeL;
}

.embPanel .embPanel__optionsMenuButton {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,13 +455,18 @@ 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 },
trigger: contextMenuTrigger,
})),
closeMenu: this.closeMyContextMenuPanel,
});

return {
panels,
actions: sortedActions,
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,24 @@ 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;
index?: number;
}

interface State {
actionContextMenuPanel?: EuiContextMenuPanelDescriptor[];
actionContextMenuPanel?: {
panels: EuiContextMenuPanelDescriptor[];
actions: Action[];
};
isPopoverOpen: boolean;
}

Expand Down Expand Up @@ -93,9 +100,16 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
/>
);

const showNotification = this.state.actionContextMenuPanel?.actions.some(
(action) => action.showNotification
);

return (
<EuiPopover
className="embPanel__optionsMenuPopover"
className={
'embPanel__optionsMenuPopover' +
(showNotification ? ' embPanel__optionsMenuPopover-notification' : '')
}
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
Expand All @@ -109,7 +123,7 @@ export class PanelOptionsMenu extends React.Component<PanelOptionsMenuProps, Sta
>
<EuiContextMenu
initialPanelId="mainMenu"
panels={this.state.actionContextMenuPanel || []}
panels={this.state.actionContextMenuPanel?.panels || []}
/>
</EuiPopover>
);
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/ui_actions/public/actions/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
2 changes: 2 additions & 0 deletions src/plugins/ui_actions/public/actions/action_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>) {
Expand Down
125 changes: 125 additions & 0 deletions src/plugins/visualizations/public/actions/edit_in_lens_action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 { 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 } 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 = 30;
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 searchFilters = vis.data.searchSource?.getField('filter');
const searchQuery = vis.data.searchSource?.getField('query');
const updatedWithMeta = {
...navigateToLensConfig,
title:
embeddable.getOutput().title ||
i18n.translate('visualizations.actions.editInLens.visulizationTitle', {
defaultMessage: '{type} visualization',
values: {
type: vis.type.title,
},
}),
savedObjectId: vis.id,
embeddableId: embeddable.id,
originatingApp: this.currentAppId,
searchFilters,
searchQuery,
};
if (navigateToLensConfig) {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: {
Expand All @@ -600,7 +601,7 @@ export class VisualizeEmbeddable
},
variables: {
embeddableTitle: this.getTitle(),
...expressionVariables,
...this.expressionVariables,
},
searchSessionId: this.input.searchSessionId,
syncColors: this.input.syncColors,
Expand Down Expand Up @@ -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');
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/visualizations/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
10 changes: 9 additions & 1 deletion src/plugins/visualizations/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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';
Expand Down Expand Up @@ -93,6 +97,7 @@ import {
setSavedObjectTagging,
} from './services';
import { VisualizeConstants } from '../common/constants';
import { EditInLensAction } from './actions/edit_in_lens_action';

/**
* Interface for this plugin's returned setup/start contracts.
Expand Down Expand Up @@ -340,6 +345,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);

Expand Down
8 changes: 8 additions & 0 deletions src/plugins/visualizations/public/triggers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
},
})),
withKibana: jest.fn((comp) => comp),
reactToUiComponent: jest.fn(),
}));

jest.mock('../../services', () => ({
Expand Down
Loading