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

[ES|QL] Creates charts from the dashboard #171973

Merged
merged 29 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c3fd071
Create the UiActions logic
stratoula Nov 27, 2023
03cc96d
Fixes CI checks
stratoula Nov 28, 2023
1cb11ee
Step 2, create the default suggestion
stratoula Nov 28, 2023
03d310f
Merge branch 'main' into create-esql-charts
stratoula Nov 29, 2023
cffdee6
attempt to fix the bundle size problem
stratoula Nov 29, 2023
2434dad
Attempt number 2
stratoula Nov 29, 2023
7fe18cd
Increase some limits
stratoula Nov 29, 2023
914f3e4
Create ES|QL charts
stratoula Nov 30, 2023
4ef12ab
Small changes
stratoula Nov 30, 2023
f8a0a1b
Merge branch 'main' into create-esql-charts
stratoula Nov 30, 2023
05016b6
Revert
stratoula Nov 30, 2023
d08890d
On cancel remove embeddable
stratoula Dec 1, 2023
0d50992
Merge branch 'main' into create-esql-charts
stratoula Dec 1, 2023
a289941
Merge branch 'main' into create-esql-charts
stratoula Dec 4, 2023
49ad466
Adds unit tests
stratoula Dec 4, 2023
168b155
Fixes
stratoula Dec 4, 2023
75d006a
Finish FT
stratoula Dec 4, 2023
b32b907
Merge branch 'main' into create-esql-charts
stratoula Dec 5, 2023
3793181
Merge branch 'main' into create-esql-charts
stratoula Dec 8, 2023
605d724
Merge branch 'main' into create-esql-charts
stratoula Dec 8, 2023
e44ef2e
Merge branch 'main' into create-esql-charts
stratoula Dec 13, 2023
c71e363
Merge branch 'main' into create-esql-charts
stratoula Dec 18, 2023
af4ebeb
Merge branch 'main' into create-esql-charts
stratoula Dec 19, 2023
0a003b8
Use the correct icon
stratoula Dec 19, 2023
7d612b3
Address PR comments
stratoula Dec 20, 2023
92cab39
Simplification of actions retrieval
stratoula Dec 20, 2023
163fcf9
Check that component is mounted
stratoula Dec 20, 2023
edabb8b
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Dec 20, 2023
a557ec6
Fixes jest test
stratoula Dec 20, 2023
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
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pageLoadAssetSize:
kibanaUsageCollection: 16463
kibanaUtils: 79713
kubernetesSecurity: 77234
lens: 39000
lens: 40000
licenseManagement: 41817
licensing: 29004
links: 44490
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 { getAddPanelActionMenuItems } from './add_panel_action_menu_items';

describe('getAddPanelActionMenuItems', () => {
it('returns the items correctly', async () => {
const registeredActions = [
{
id: 'ACTION_CREATE_ESQL_CHART',
type: 'ACTION_CREATE_ESQL_CHART',
getDisplayName: () => 'Action name',
getIconType: () => 'pencil',
getDisplayNameTooltip: () => 'Action tooltip',
isCompatible: () => Promise.resolve(true),
execute: jest.fn(),
},
];
const items = getAddPanelActionMenuItems(registeredActions, jest.fn(), jest.fn(), jest.fn());
expect(items).toStrictEqual([
{
'data-test-subj': 'create-action-Action name',
icon: 'pencil',
name: 'Action name',
onClick: expect.any(Function),
toolTipContent: 'Action tooltip',
},
]);
});

it('returns empty array if no actions have been registered', async () => {
const items = getAddPanelActionMenuItems([], jest.fn(), jest.fn(), jest.fn());
expect(items).toStrictEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 type { ActionExecutionContext, Action } from '@kbn/ui-actions-plugin/public';
import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { addPanelMenuTrigger } from '../../triggers';

const onAddPanelActionClick =
(action: Action, context: ActionExecutionContext<object>, closePopover: () => void) =>
(event: React.MouseEvent) => {
closePopover();
if (event.currentTarget instanceof HTMLAnchorElement) {
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 &&
(!event.currentTarget.target || event.currentTarget.target === '_self') &&
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
) {
event.preventDefault();
action.execute(context);
}
} else action.execute(context);
};

export const getAddPanelActionMenuItems = (
actions: Array<Action<object>> | undefined,
createNewEmbeddable: (embeddableFactory: EmbeddableFactory) => void,
deleteEmbeddable: (embeddableId: string) => void,
closePopover: () => void
) => {
const actionsWithContext =
actions?.map((action) => ({
action,
context: {
createNewEmbeddable,
deleteEmbeddable,
},
trigger: addPanelMenuTrigger,
})) ?? [];

return actionsWithContext?.map((item) => {
const context: ActionExecutionContext<object> = {
...item.context,
trigger: item.trigger,
};
const actionName = item.action.getDisplayName(context);
return {
name: actionName,
icon: item.action.getIconType(context),
onClick: onAddPanelActionClick(item.action, context, closePopover),
'data-test-subj': `create-action-${actionName}`,
toolTipContent: item.action?.getDisplayNameTooltip?.(context),
};
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,37 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
[stateTransferService, dashboard, search.session, trackUiMetric]
);

/**
* embeddableFactory: Required, you can get the factory from embeddableStart.getEmbeddableFactory(<embeddable type, i.e. lens>)
* initialInput: Optional, use it in case you want to pass your own input to the factory
* dismissNotification: Optional, if not passed a toast will appear in the dashboard
*/
const createNewEmbeddable = useCallback(
async (embeddableFactory: EmbeddableFactory) => {
async (
embeddableFactory: EmbeddableFactory,
initialInput?: Partial<EmbeddableInput>,
dismissNotification?: boolean
) => {
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, embeddableFactory.type);
}

let explicitInput: Partial<EmbeddableInput>;
let attributes: unknown;
try {
const explicitInputReturn = await embeddableFactory.getExplicitInput(undefined, dashboard);
if (isExplicitInputWithAttributes(explicitInputReturn)) {
explicitInput = explicitInputReturn.newInput;
attributes = explicitInputReturn.attributes;
if (initialInput) {
explicitInput = initialInput;
} else {
explicitInput = explicitInputReturn;
const explicitInputReturn = await embeddableFactory.getExplicitInput(
undefined,
dashboard
);
if (isExplicitInputWithAttributes(explicitInputReturn)) {
explicitInput = explicitInputReturn.newInput;
attributes = explicitInputReturn.attributes;
} else {
explicitInput = explicitInputReturn;
}
}
} catch (e) {
// error likely means user canceled embeddable creation
Expand All @@ -110,19 +126,31 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
if (newEmbeddable) {
dashboard.setScrollToPanelId(newEmbeddable.id);
dashboard.setHighlightPanelId(newEmbeddable.id);
toasts.addSuccess({
title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()),
'data-test-subj': 'addEmbeddableToDashboardSuccess',
});

if (!dismissNotification) {
toasts.addSuccess({
title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()),
'data-test-subj': 'addEmbeddableToDashboardSuccess',
});
}
}
return newEmbeddable;
},
[trackUiMetric, dashboard, toasts]
);

const deleteEmbeddable = useCallback(
(embeddableId: string) => {
dashboard.removeEmbeddable(embeddableId);
},
[dashboard]
);

const extraButtons = [
<EditorMenu
createNewVisType={createNewVisType}
createNewEmbeddable={createNewEmbeddable}
deleteEmbeddable={deleteEmbeddable}
isDisabled={isDisabled}
/>,
<AddFromLibraryButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,22 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ToolbarPopover } from '@kbn/shared-ux-button-toolbar';
import type { Action } from '@kbn/ui-actions-plugin/public';
import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public';
import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { pluginServices } from '../../services/plugin_services';
import { DASHBOARD_APP_ID } from '../../dashboard_constants';
import { ADD_PANEL_TRIGGER } from '../../triggers';
import { getAddPanelActionMenuItems } from './add_panel_action_menu_items';

interface Props {
isDisabled?: boolean;
/** Handler for creating new visualization of a specified type */
createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void;
/** Handler for creating a new embeddable of a specified type */
createNewEmbeddable: (embeddableFactory: EmbeddableFactory) => void;
/** Handler for deleting an embeddable */
deleteEmbeddable: (embeddableId: string) => void;
}

interface FactoryGroup {
Expand All @@ -44,14 +49,20 @@ interface UnwrappedEmbeddableFactory {
isEditable: boolean;
}

export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled }: Props) => {
export const EditorMenu = ({
createNewVisType,
createNewEmbeddable,
deleteEmbeddable,
isDisabled,
}: Props) => {
const {
embeddable,
visualizations: {
getAliases: getVisTypeAliases,
getByGroup: getVisTypesByGroup,
showNewVisModal,
},
uiActions,
} = pluginServices.getServices();

const { euiTheme } = useEuiTheme();
Expand All @@ -64,6 +75,10 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled }
UnwrappedEmbeddableFactory[]
>([]);

const [addPanelActions, setAddPanelActions] = useState<Array<Action<object>> | undefined>(
undefined
);

useEffect(() => {
Promise.all(
embeddableFactories.map<Promise<UnwrappedEmbeddableFactory>>(async (factory) => ({
Expand Down Expand Up @@ -121,6 +136,18 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled }

let panelCount = 1 + aggBasedPanelID;

// Retrieve ADD_PANEL_TRIGGER actions
useEffect(() => {
async function loadPanelActions() {
const registeredActions = await uiActions?.getTriggerCompatibleActions?.(
ADD_PANEL_TRIGGER,
{}
);
setAddPanelActions(registeredActions);
}
loadPanelActions();
}, [uiActions]);

factories.forEach(({ factory }) => {
const { grouping } = factory;

Expand Down Expand Up @@ -236,6 +263,12 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled }
})),

...promotedVisTypes.map(getVisTypeMenuItem),
...getAddPanelActionMenuItems(
addPanelActions,
createNewEmbeddable,
deleteEmbeddable,
closePopover
),
];
if (aggsBasedVisTypes.length > 0) {
initialPanelItems.push({
Expand Down
15 changes: 14 additions & 1 deletion src/plugins/dashboard/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
import { DashboardMountContextProps } from './dashboard_app/types';
import type { FindDashboardsService } from './services/dashboard_content_management/types';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
import { addPanelMenuTrigger } from './triggers';

export interface DashboardFeatureFlagConfig {
allowByValueEmbeddables: boolean;
Expand Down Expand Up @@ -149,11 +150,23 @@ export class DashboardPlugin

public setup(
core: CoreSetup<DashboardStartDependencies, DashboardStart>,
{ share, embeddable, home, urlForwarding, data, contentManagement }: DashboardSetupDependencies
{
share,
embeddable,
home,
urlForwarding,
data,
contentManagement,
uiActions,
}: DashboardSetupDependencies
): DashboardSetup {
this.dashboardFeatureFlagConfig =
this.initializerContext.config.get<DashboardFeatureFlagConfig>();

// this trigger enables external consumers to register actions for
// adding items to the add panel menu
uiActions.registerTrigger(addPanelMenuTrigger);

if (share) {
this.locator = share.url.locators.create(
new DashboardAppLocatorDefinition({
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/dashboard/public/services/plugin_services.stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { savedObjectsManagementServiceFactory } from './saved_objects_management
import { contentManagementServiceFactory } from './content_management/content_management_service.stub';
import { serverlessServiceFactory } from './serverless/serverless_service.stub';
import { noDataPageServiceFactory } from './no_data_page/no_data_page_service.stub';
import { uiActionsServiceFactory } from './ui_actions/ui_actions_service.stub';

export const providers: PluginServiceProviders<DashboardServices> = {
dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory),
Expand Down Expand Up @@ -74,6 +75,7 @@ export const providers: PluginServiceProviders<DashboardServices> = {
contentManagement: new PluginServiceProvider(contentManagementServiceFactory),
serverless: new PluginServiceProvider(serverlessServiceFactory),
noDataPage: new PluginServiceProvider(noDataPageServiceFactory),
uiActions: new PluginServiceProvider(uiActionsServiceFactory),
};

export const registry = new PluginServiceRegistry<DashboardServices>(providers);
2 changes: 2 additions & 0 deletions src/plugins/dashboard/public/services/plugin_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { dashboardContentManagementServiceFactory } from './dashboard_content_ma
import { contentManagementServiceFactory } from './content_management/content_management_service';
import { serverlessServiceFactory } from './serverless/serverless_service';
import { noDataPageServiceFactory } from './no_data_page/no_data_page_service';
import { uiActionsServiceFactory } from './ui_actions/ui_actions_service';

const providers: PluginServiceProviders<DashboardServices, DashboardPluginServiceParams> = {
dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [
Expand Down Expand Up @@ -88,6 +89,7 @@ const providers: PluginServiceProviders<DashboardServices, DashboardPluginServic
contentManagement: new PluginServiceProvider(contentManagementServiceFactory),
serverless: new PluginServiceProvider(serverlessServiceFactory),
noDataPage: new PluginServiceProvider(noDataPageServiceFactory),
uiActions: new PluginServiceProvider(uiActionsServiceFactory),
};

export const pluginServices = new PluginServices<DashboardServices>();
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/dashboard/public/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { DashboardUsageCollectionService } from './usage_collection/types';
import { DashboardVisualizationsService } from './visualizations/types';
import { DashboardServerlessService } from './serverless/types';
import { NoDataPageService } from './no_data_page/types';
import { DashboardUiActionsService } from './ui_actions/types';

export type DashboardPluginServiceParams = KibanaPluginServiceParams<DashboardStartDependencies> & {
initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext
Expand Down Expand Up @@ -74,4 +75,5 @@ export interface DashboardServices {
contentManagement: ContentManagementPublicStart;
serverless: DashboardServerlessService; // TODO: make this optional in follow up
noDataPage: NoDataPageService;
uiActions: DashboardUiActionsService;
}
13 changes: 13 additions & 0 deletions src/plugins/dashboard/public/services/ui_actions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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 type { UiActionsStart } from '@kbn/ui-actions-plugin/public';

export interface DashboardUiActionsService {
getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions'];
}
Original file line number Diff line number Diff line change
@@ -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 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 { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardUiActionsService } from './types';

export type UIActionsServiceFactory = PluginServiceFactory<DashboardUiActionsService>;

export const uiActionsServiceFactory: UIActionsServiceFactory = () => {
const pluginMock = uiActionsPluginMock.createStartContract();
return { getTriggerCompatibleActions: pluginMock.getTriggerCompatibleActions };
};
Loading