mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[dashboard] refactor EditorMenu to AddPanelButton (#208226)
PR refactors `EditorMenu` component into `AddPanelButton` to provide better naming scheme and simplify logic. * replace `COMMON_VISUALIZATION_GROUPING` with `ADD_PANEL_VISUALIZATION_GROUP`. Moved from visualizations plugin to embeddable plugin. * rename `EditorMenu` => `AddPanelButton` * rename `DashboardPanelSelectionListFlyout` => `AddPanelFlyout` * remove unused style `dshSolutionToolbar__editorContextMenu` from editor_menu.scss * Simplify loading of `AddPanelFlyout` contents. Replaced `useGetDashboardPanels` hook with `getMenuItemGroups` function. * avoid loading `AddPanelFlyout` contents until `AddPanelButton` is clicked. `DashboardEditingToolbar` component used to setup `useGetDashboardPanels` hook. Now `AddPanelFlyout` loads contents by directly calling `getMenuItemGroups`. * Added loading state to `AddPanelFlyout` <img width="600" alt="Screenshot 2025-01-24 at 9 21 05 AM" src="https://github.com/user-attachments/assets/b2dfde5f-e347-4745-9ee0-b3268e2138c2" /> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
This commit is contained in:
parent
8ba2179096
commit
dcf64f2eb6
31 changed files with 780 additions and 1288 deletions
|
@ -7,5 +7,5 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { useGetDashboardPanels } from './use_get_dashboard_panels';
|
||||
export { DashboardPanelSelectionListFlyout } from './dashboard_panel_selection_flyout';
|
||||
export { getLinksEmbeddableFactory } from './links_embeddable';
|
||||
export { deserializeLinksSavedObject } from '../lib/deserialize_from_library';
|
|
@ -1,116 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
|
||||
import { getAddPanelActionMenuItemsGroup } 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(),
|
||||
},
|
||||
{
|
||||
id: 'TEST_ACTION_01',
|
||||
type: 'TEST_ACTION_01',
|
||||
getDisplayName: () => 'Action name 01',
|
||||
getIconType: () => 'pencil',
|
||||
getDisplayNameTooltip: () => 'Action tooltip',
|
||||
isCompatible: () => Promise.resolve(true),
|
||||
execute: jest.fn(),
|
||||
grouping: [
|
||||
{
|
||||
id: 'groupedAddPanelAction',
|
||||
getDisplayName: () => 'Custom group',
|
||||
getIconType: () => 'logoElasticsearch',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'TEST_ACTION_02',
|
||||
type: 'TEST_ACTION_02',
|
||||
getDisplayName: () => 'Action name',
|
||||
getDisplayNameTooltip: () => 'Action tooltip',
|
||||
getIconType: () => undefined,
|
||||
isCompatible: () => Promise.resolve(true),
|
||||
execute: jest.fn(),
|
||||
grouping: [
|
||||
{
|
||||
id: 'groupedAddPanelAction',
|
||||
getDisplayName: () => 'Custom group',
|
||||
getIconType: () => 'logoElasticsearch',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const grouped = getAddPanelActionMenuItemsGroup(
|
||||
getMockPresentationContainer(),
|
||||
registeredActions,
|
||||
jest.fn()
|
||||
);
|
||||
|
||||
expect(grouped).toStrictEqual({
|
||||
groupedAddPanelAction: {
|
||||
id: 'groupedAddPanelAction',
|
||||
title: 'Custom group',
|
||||
order: 0,
|
||||
'data-test-subj': 'dashboardEditorMenu-groupedAddPanelActionGroup',
|
||||
items: [
|
||||
{
|
||||
'data-test-subj': 'create-action-Action name 01',
|
||||
icon: 'pencil',
|
||||
id: 'TEST_ACTION_01',
|
||||
name: 'Action name 01',
|
||||
onClick: expect.any(Function),
|
||||
description: 'Action tooltip',
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'create-action-Action name',
|
||||
icon: 'empty',
|
||||
id: 'TEST_ACTION_02',
|
||||
name: 'Action name',
|
||||
onClick: expect.any(Function),
|
||||
description: 'Action tooltip',
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
other: {
|
||||
id: 'other',
|
||||
title: 'Other',
|
||||
order: -1,
|
||||
'data-test-subj': 'dashboardEditorMenu-otherGroup',
|
||||
items: [
|
||||
{
|
||||
id: 'ACTION_CREATE_ESQL_CHART',
|
||||
name: 'Action name',
|
||||
icon: 'pencil',
|
||||
description: 'Action tooltip',
|
||||
onClick: expect.any(Function),
|
||||
'data-test-subj': 'create-action-Action name',
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array if no actions have been registered', async () => {
|
||||
const grouped = getAddPanelActionMenuItemsGroup(getMockPresentationContainer(), [], jest.fn());
|
||||
|
||||
expect(grouped).toStrictEqual({});
|
||||
});
|
||||
});
|
|
@ -1,118 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
type ActionExecutionContext,
|
||||
type Action,
|
||||
addPanelMenuTrigger,
|
||||
} from '@kbn/ui-actions-plugin/public';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { ADD_PANEL_OTHER_GROUP } from '@kbn/embeddable-plugin/public';
|
||||
import type { IconType, CommonProps } from '@elastic/eui';
|
||||
import React, { type MouseEventHandler } from 'react';
|
||||
|
||||
export interface PanelSelectionMenuItem extends Pick<CommonProps, 'data-test-subj'> {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: IconType;
|
||||
onClick: MouseEventHandler;
|
||||
description?: string;
|
||||
isDisabled?: boolean;
|
||||
isDeprecated?: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export type GroupedAddPanelActions = Pick<
|
||||
PanelSelectionMenuItem,
|
||||
'id' | 'isDisabled' | 'data-test-subj' | 'order'
|
||||
> & {
|
||||
title: string;
|
||||
items: PanelSelectionMenuItem[];
|
||||
};
|
||||
|
||||
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 getAddPanelActionMenuItemsGroup = (
|
||||
api: PresentationContainer,
|
||||
actions: Array<Action<object>> | undefined,
|
||||
onPanelSelected: () => void
|
||||
) => {
|
||||
const grouped: Record<string, GroupedAddPanelActions> = {};
|
||||
|
||||
const context = {
|
||||
embeddable: api,
|
||||
trigger: addPanelMenuTrigger,
|
||||
};
|
||||
|
||||
const getMenuItem = (item: Action<object>): PanelSelectionMenuItem => {
|
||||
const actionName = item.getDisplayName(context);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: actionName,
|
||||
icon:
|
||||
(typeof item.getIconType === 'function' ? item.getIconType(context) : undefined) ?? 'empty',
|
||||
onClick: onAddPanelActionClick(item, context, onPanelSelected),
|
||||
'data-test-subj': `create-action-${actionName}`,
|
||||
description: item?.getDisplayNameTooltip?.(context),
|
||||
order: item.order ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
actions?.forEach((item) => {
|
||||
if (Array.isArray(item.grouping)) {
|
||||
item.grouping.forEach((group) => {
|
||||
const groupId = group.id;
|
||||
if (!grouped[groupId]) {
|
||||
grouped[groupId] = {
|
||||
id: groupId,
|
||||
title: group.getDisplayName ? group.getDisplayName(context) : '',
|
||||
'data-test-subj': `dashboardEditorMenu-${groupId}Group`,
|
||||
order: group.order ?? 0,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
grouped[group.id]!.items!.push(getMenuItem(item));
|
||||
});
|
||||
} else {
|
||||
// use other group as the default for definitions that don't have a group
|
||||
const fallbackGroup = ADD_PANEL_OTHER_GROUP;
|
||||
|
||||
if (!grouped[fallbackGroup.id]) {
|
||||
grouped[fallbackGroup.id] = {
|
||||
id: fallbackGroup.id,
|
||||
title: fallbackGroup.getDisplayName?.() || '',
|
||||
'data-test-subj': `dashboardEditorMenu-${fallbackGroup.id}Group`,
|
||||
order: fallbackGroup.order || 0,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
grouped[fallbackGroup.id].items.push(getMenuItem(item));
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
};
|
|
@ -1,109 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { type ComponentProps } from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { DashboardPanelSelectionListFlyout } from './dashboard_panel_selection_flyout';
|
||||
import type { GroupedAddPanelActions } from './add_panel_action_menu_items';
|
||||
|
||||
const defaultProps: Omit<
|
||||
ComponentProps<typeof DashboardPanelSelectionListFlyout>,
|
||||
'fetchDashboardPanels'
|
||||
> = {
|
||||
close: jest.fn(),
|
||||
paddingSize: 's',
|
||||
};
|
||||
|
||||
const renderComponent = ({
|
||||
fetchDashboardPanels,
|
||||
}: Pick<ComponentProps<typeof DashboardPanelSelectionListFlyout>, 'fetchDashboardPanels'>) =>
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<DashboardPanelSelectionListFlyout
|
||||
{...defaultProps}
|
||||
fetchDashboardPanels={fetchDashboardPanels}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const panelConfiguration: GroupedAddPanelActions[] = [
|
||||
{
|
||||
id: 'panel1',
|
||||
title: 'App 1',
|
||||
items: [
|
||||
{
|
||||
icon: 'icon1',
|
||||
id: 'mockFactory',
|
||||
name: 'Factory 1',
|
||||
description: 'Factory 1 description',
|
||||
'data-test-subj': 'createNew-mockFactory',
|
||||
onClick: jest.fn(),
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
order: 10,
|
||||
'data-test-subj': 'dashboardEditorMenu-group1Group',
|
||||
},
|
||||
];
|
||||
|
||||
describe('DashboardPanelSelectionListFlyout', () => {
|
||||
it('renders a loading indicator when fetchDashboardPanel has not yielded any value', async () => {
|
||||
const promiseDelay = 5000;
|
||||
|
||||
renderComponent({
|
||||
fetchDashboardPanels: jest.fn(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => resolve(panelConfiguration), promiseDelay);
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('dashboardPanelSelectionLoadingIndicator')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an error indicator when fetchDashboardPanel errors', async () => {
|
||||
renderComponent({
|
||||
fetchDashboardPanels: jest.fn().mockRejectedValue(new Error('simulated error')),
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('dashboardPanelSelectionErrorIndicator')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the list of available panels when fetchDashboardPanel resolves a value', async () => {
|
||||
renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) });
|
||||
|
||||
expect(await screen.findByTestId(panelConfiguration[0]['data-test-subj']!)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a not found message when a user searches for an item that is not in the selection list', async () => {
|
||||
renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) });
|
||||
|
||||
expect(await screen.findByTestId(panelConfiguration[0]['data-test-subj']!)).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByTestId('dashboardPanelSelectionFlyout__searchInput'),
|
||||
'non existent panel'
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('dashboardPanelSelectionNoPanelMessage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('invokes the close method when the flyout close btn is clicked', async () => {
|
||||
renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) });
|
||||
|
||||
fireEvent.click(await screen.findByTestId('dashboardPanelSelectionCloseBtn'));
|
||||
|
||||
expect(defaultProps.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,293 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { i18n as i18nFn } from '@kbn/i18n';
|
||||
import { type EuiFlyoutProps, EuiLoadingChart } from '@elastic/eui';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiForm,
|
||||
EuiBadge,
|
||||
EuiFormRow,
|
||||
EuiTitle,
|
||||
EuiFieldSearch,
|
||||
useEuiTheme,
|
||||
EuiListGroup,
|
||||
EuiListGroupItem,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
type PanelSelectionMenuItem,
|
||||
type GroupedAddPanelActions,
|
||||
} from './add_panel_action_menu_items';
|
||||
|
||||
export interface DashboardPanelSelectionListFlyoutProps {
|
||||
/** Handler to close flyout */
|
||||
close: () => void;
|
||||
/** Padding for flyout */
|
||||
paddingSize: Exclude<EuiFlyoutProps['paddingSize'], 'none' | undefined>;
|
||||
/** Fetches the panels available for a dashboard */
|
||||
fetchDashboardPanels: () => Promise<GroupedAddPanelActions[]>;
|
||||
}
|
||||
|
||||
export const DashboardPanelSelectionListFlyout: React.FC<
|
||||
DashboardPanelSelectionListFlyoutProps
|
||||
> = ({ close, paddingSize, fetchDashboardPanels }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [{ data: panels, loading, error }, setPanelState] = useState<{
|
||||
loading: boolean;
|
||||
data: GroupedAddPanelActions[] | null;
|
||||
error: unknown | null;
|
||||
}>({ loading: true, data: null, error: null });
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [panelsSearchResult, setPanelsSearchResult] = useState<GroupedAddPanelActions[] | null>(
|
||||
panels
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const requestDashboardPanels = () => {
|
||||
fetchDashboardPanels()
|
||||
.then((_panels) =>
|
||||
setPanelState((prevState) => ({
|
||||
...prevState,
|
||||
loading: false,
|
||||
data: _panels,
|
||||
}))
|
||||
)
|
||||
.catch((err) =>
|
||||
setPanelState((prevState) => ({
|
||||
...prevState,
|
||||
loading: false,
|
||||
error: err,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
requestDashboardPanels();
|
||||
}, [fetchDashboardPanels]);
|
||||
|
||||
useEffect(() => {
|
||||
const _panels = (panels ?? []).slice(0);
|
||||
|
||||
if (!searchTerm) {
|
||||
return setPanelsSearchResult(_panels);
|
||||
}
|
||||
|
||||
const q = searchTerm.toLowerCase();
|
||||
|
||||
setPanelsSearchResult(
|
||||
orderBy(
|
||||
_panels.map((panel) => {
|
||||
const groupSearchMatch = panel.title.toLowerCase().includes(q);
|
||||
|
||||
const [groupSearchMatchAgg, items] = panel.items.reduce(
|
||||
(acc, cur) => {
|
||||
const searchMatch = cur.name.toLowerCase().includes(q);
|
||||
|
||||
acc[0] = acc[0] || searchMatch;
|
||||
acc[1].push({
|
||||
...cur,
|
||||
isDisabled: !(groupSearchMatch || searchMatch),
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
[groupSearchMatch, [] as PanelSelectionMenuItem[]]
|
||||
);
|
||||
|
||||
return {
|
||||
...panel,
|
||||
isDisabled: !groupSearchMatchAgg,
|
||||
items,
|
||||
};
|
||||
}),
|
||||
['isDisabled']
|
||||
)
|
||||
);
|
||||
}, [panels, searchTerm]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h1 id="addPanelsFlyout">
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.headingText"
|
||||
defaultMessage="Add panel"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup direction="column" responsive={false} gutterSize="m">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={{
|
||||
position: 'sticky',
|
||||
top: euiTheme.size[paddingSize],
|
||||
zIndex: 1,
|
||||
boxShadow: `0 -${euiTheme.size[paddingSize]} 0 4px ${euiTheme.colors.emptyShade}`,
|
||||
}}
|
||||
>
|
||||
<EuiForm component="form" fullWidth>
|
||||
<EuiFormRow css={{ backgroundColor: euiTheme.colors.emptyShade }}>
|
||||
<EuiFieldSearch
|
||||
compressed
|
||||
autoFocus
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
aria-label={i18nFn.translate(
|
||||
'dashboard.editorMenu.addPanelFlyout.searchLabelText',
|
||||
{ defaultMessage: 'search field for panels' }
|
||||
)}
|
||||
className="nsPanelSelectionFlyout__searchInput"
|
||||
data-test-subj="dashboardPanelSelectionFlyout__searchInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={{
|
||||
minHeight: '20vh',
|
||||
...(loading || error
|
||||
? {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="dashboardPanelSelectionLoadingIndicator"
|
||||
icon={<EuiLoadingChart size="l" mono />}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="m"
|
||||
data-test-subj="dashboardPanelSelectionList"
|
||||
>
|
||||
{panelsSearchResult?.some(({ isDisabled }) => !isDisabled) ? (
|
||||
panelsSearchResult.map(
|
||||
({ id, title, items, isDisabled, ['data-test-subj']: dataTestSubj, order }) =>
|
||||
!isDisabled ? (
|
||||
<EuiFlexItem
|
||||
key={id}
|
||||
data-test-subj={dataTestSubj}
|
||||
data-group-sort-order={order}
|
||||
>
|
||||
<EuiTitle id={`${id}-group`} size="xxs">
|
||||
{typeof title === 'string' ? <h3>{title}</h3> : title}
|
||||
</EuiTitle>
|
||||
<EuiListGroup
|
||||
aria-labelledby={`${id}-group`}
|
||||
size="s"
|
||||
gutterSize="none"
|
||||
maxWidth={false}
|
||||
flush
|
||||
>
|
||||
{items?.map((item, idx) => {
|
||||
return (
|
||||
<EuiListGroupItem
|
||||
key={`${id}.${idx}`}
|
||||
label={
|
||||
<EuiToolTip position="right" content={item.description}>
|
||||
{!item.isDeprecated ? (
|
||||
<EuiText size="s">{item.name}</EuiText>
|
||||
) : (
|
||||
<EuiFlexGroup wrap responsive={false} gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{item.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="warning">
|
||||
<FormattedMessage
|
||||
id="dashboard.editorMenu.deprecatedTag"
|
||||
defaultMessage="Deprecated"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiToolTip>
|
||||
}
|
||||
onClick={item?.onClick}
|
||||
iconType={item.icon}
|
||||
data-test-subj={item['data-test-subj']}
|
||||
isDisabled={item.isDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiListGroup>
|
||||
</EuiFlexItem>
|
||||
) : null
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{Boolean(error) ? (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
iconColor="danger"
|
||||
body={
|
||||
<EuiText size="s" textAlign="center">
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.loadingErrorDescription"
|
||||
defaultMessage="An error occurred loading the available dashboard panels for selection"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
data-test-subj="dashboardPanelSelectionErrorIndicator"
|
||||
/>
|
||||
) : (
|
||||
<EuiText
|
||||
size="s"
|
||||
textAlign="center"
|
||||
data-test-subj="dashboardPanelSelectionNoPanelMessage"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.noResultsDescription"
|
||||
defaultMessage="No panel types found"
|
||||
/>
|
||||
</EuiText>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={close} data-test-subj="dashboardPanelSelectionCloseBtn">
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.cancelButtonText"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,218 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ADD_PANEL_ANNOTATION_GROUP, ADD_PANEL_LEGACY_GROUP } from '@kbn/embeddable-plugin/public';
|
||||
import type { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import type { Action, UiActionsService } from '@kbn/ui-actions-plugin/public';
|
||||
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
VisGroups,
|
||||
VisTypeAlias,
|
||||
VisualizationsStart,
|
||||
type BaseVisType,
|
||||
} from '@kbn/visualizations-plugin/public';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { uiActionsService, visualizationsService } from '../../../services/kibana_services';
|
||||
import { useGetDashboardPanels } from './use_get_dashboard_panels';
|
||||
|
||||
const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked<PresentationContainer>;
|
||||
|
||||
describe('Get dashboard panels hook', () => {
|
||||
const defaultHookProps: Parameters<typeof useGetDashboardPanels>[0] = {
|
||||
api: mockApi,
|
||||
createNewVisType: jest.fn(),
|
||||
};
|
||||
|
||||
let compatibleTriggerActionsRequestSpy: jest.SpyInstance<
|
||||
ReturnType<NonNullable<UiActionsService['getTriggerCompatibleActions']>>
|
||||
>;
|
||||
|
||||
let dashboardVisualizationGroupGetterSpy: jest.SpyInstance<
|
||||
ReturnType<VisualizationsStart['getByGroup']>
|
||||
>;
|
||||
|
||||
let dashboardVisualizationAliasesGetterSpy: jest.SpyInstance<
|
||||
ReturnType<VisualizationsStart['getAliases']>
|
||||
>;
|
||||
|
||||
beforeAll(() => {
|
||||
compatibleTriggerActionsRequestSpy = jest.spyOn(
|
||||
uiActionsService,
|
||||
'getTriggerCompatibleActions'
|
||||
);
|
||||
dashboardVisualizationGroupGetterSpy = jest.spyOn(visualizationsService, 'getByGroup');
|
||||
dashboardVisualizationAliasesGetterSpy = jest.spyOn(visualizationsService, 'getAliases');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
compatibleTriggerActionsRequestSpy.mockResolvedValue([]);
|
||||
dashboardVisualizationGroupGetterSpy.mockReturnValue([]);
|
||||
dashboardVisualizationAliasesGetterSpy.mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('useGetDashboardPanels', () => {
|
||||
it('hook return value is callable', () => {
|
||||
const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps));
|
||||
expect(result.current).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('returns a callable method that yields a cached result if invoked after a prior resolution', async () => {
|
||||
const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps));
|
||||
expect(result.current).toBeInstanceOf(Function);
|
||||
|
||||
const firstInvocationResult = await result.current(jest.fn());
|
||||
|
||||
expect(compatibleTriggerActionsRequestSpy).toHaveBeenCalledWith(ADD_PANEL_TRIGGER, {
|
||||
embeddable: expect.objectContaining(mockApi),
|
||||
});
|
||||
|
||||
const secondInvocationResult = await result.current(jest.fn());
|
||||
|
||||
expect(firstInvocationResult).toStrictEqual(secondInvocationResult);
|
||||
|
||||
expect(compatibleTriggerActionsRequestSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('augmenting ui action group items with dashboard visualization types', () => {
|
||||
it.each([
|
||||
['visualizations', VisGroups.PROMOTED],
|
||||
[ADD_PANEL_LEGACY_GROUP.id, VisGroups.LEGACY],
|
||||
[ADD_PANEL_ANNOTATION_GROUP.id, VisGroups.TOOLS],
|
||||
])(
|
||||
'includes in the ui action %s group, %s dashboard visualization group types',
|
||||
async (uiActionGroupId, dashboardVisualizationGroupId) => {
|
||||
const mockVisualizationsUiAction: Action<object> = {
|
||||
id: `some-${uiActionGroupId}-action`,
|
||||
type: '',
|
||||
order: 10,
|
||||
grouping: [
|
||||
{
|
||||
id: uiActionGroupId,
|
||||
order: 1000,
|
||||
getDisplayName: jest.fn(),
|
||||
getIconType: jest.fn(),
|
||||
},
|
||||
],
|
||||
getDisplayName: jest.fn(() => `Some ${uiActionGroupId} visualization Action`),
|
||||
getIconType: jest.fn(),
|
||||
execute: jest.fn(),
|
||||
isCompatible: jest.fn(() => Promise.resolve(true)),
|
||||
};
|
||||
|
||||
const mockDashboardVisualizationType = {
|
||||
name: dashboardVisualizationGroupId,
|
||||
title: dashboardVisualizationGroupId,
|
||||
order: 0,
|
||||
description: `This is a dummy representation of a ${dashboardVisualizationGroupId} visualization.`,
|
||||
icon: 'empty',
|
||||
stage: 'production',
|
||||
isDeprecated: false,
|
||||
group: dashboardVisualizationGroupId,
|
||||
titleInWizard: `Custom ${dashboardVisualizationGroupId} visualization`,
|
||||
} as BaseVisType;
|
||||
|
||||
compatibleTriggerActionsRequestSpy.mockResolvedValue([mockVisualizationsUiAction]);
|
||||
|
||||
dashboardVisualizationGroupGetterSpy.mockImplementation((group) => {
|
||||
if (group !== dashboardVisualizationGroupId) return [];
|
||||
|
||||
return [mockDashboardVisualizationType];
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps));
|
||||
expect(result.current).toBeInstanceOf(Function);
|
||||
|
||||
expect(await result.current(jest.fn())).toStrictEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: uiActionGroupId,
|
||||
'data-test-subj': `dashboardEditorMenu-${uiActionGroupId}Group`,
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
// @ts-expect-error ignore passing the required context in this test
|
||||
'data-test-subj': `create-action-${mockVisualizationsUiAction.getDisplayName()}`,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
'data-test-subj': `visType-${mockDashboardVisualizationType.name}`,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('includes in the ui action visualization group dashboard visualization alias types', async () => {
|
||||
const mockVisualizationsUiAction: Action<object> = {
|
||||
id: 'some-vis-action',
|
||||
type: '',
|
||||
order: 10,
|
||||
grouping: [
|
||||
{
|
||||
id: 'visualizations',
|
||||
order: 1000,
|
||||
getDisplayName: jest.fn(),
|
||||
getIconType: jest.fn(),
|
||||
},
|
||||
],
|
||||
getDisplayName: jest.fn(() => 'Some visualization Action'),
|
||||
getIconType: jest.fn(),
|
||||
execute: jest.fn(),
|
||||
isCompatible: jest.fn(() => Promise.resolve(true)),
|
||||
};
|
||||
|
||||
const mockedAliasVisualizationType: VisTypeAlias = {
|
||||
name: 'alias visualization',
|
||||
title: 'Alias Visualization',
|
||||
order: 0,
|
||||
description: 'This is a dummy representation of aan aliased visualization.',
|
||||
icon: 'empty',
|
||||
stage: 'production',
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
compatibleTriggerActionsRequestSpy.mockResolvedValue([mockVisualizationsUiAction]);
|
||||
|
||||
dashboardVisualizationAliasesGetterSpy.mockReturnValue([mockedAliasVisualizationType]);
|
||||
|
||||
const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps));
|
||||
expect(result.current).toBeInstanceOf(Function);
|
||||
|
||||
expect(await result.current(jest.fn())).toStrictEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: mockVisualizationsUiAction.grouping![0].id,
|
||||
'data-test-subj': `dashboardEditorMenu-${
|
||||
mockVisualizationsUiAction.grouping![0].id
|
||||
}Group`,
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
// @ts-expect-error ignore passing the required context in this test
|
||||
'data-test-subj': `create-action-${mockVisualizationsUiAction.getDisplayName()}`,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
'data-test-subj': `visType-${mockedAliasVisualizationType.name}`,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,210 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { AsyncSubject, defer, from, lastValueFrom, map, type Subscription } from 'rxjs';
|
||||
|
||||
import { ADD_PANEL_ANNOTATION_GROUP, ADD_PANEL_LEGACY_GROUP } from '@kbn/embeddable-plugin/public';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||
import { VisGroups, type BaseVisType, type VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
|
||||
import { uiActionsService, visualizationsService } from '../../../services/kibana_services';
|
||||
import {
|
||||
getAddPanelActionMenuItemsGroup,
|
||||
type GroupedAddPanelActions,
|
||||
type PanelSelectionMenuItem,
|
||||
} from './add_panel_action_menu_items';
|
||||
|
||||
interface UseGetDashboardPanelsArgs {
|
||||
api: PresentationContainer;
|
||||
createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void;
|
||||
}
|
||||
|
||||
const sortGroupPanelsByOrder = <T extends { order: number }>(panelGroups: T[]): T[] => {
|
||||
return panelGroups.sort(
|
||||
// larger number sorted to the top
|
||||
(panelGroupA, panelGroupB) => panelGroupB.order - panelGroupA.order
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetDashboardPanels = ({ api, createNewVisType }: UseGetDashboardPanelsArgs) => {
|
||||
const panelsComputeResultCache = useRef(new AsyncSubject<GroupedAddPanelActions[]>());
|
||||
const panelsComputeSubscription = useRef<Subscription | null>(null);
|
||||
|
||||
const getSortedVisTypesByGroup = (group: VisGroups) =>
|
||||
visualizationsService
|
||||
.getByGroup(group)
|
||||
.sort((a: BaseVisType | VisTypeAlias, b: BaseVisType | VisTypeAlias) => {
|
||||
const labelA = 'titleInWizard' in a ? a.titleInWizard || a.title : a.title;
|
||||
const labelB = 'titleInWizard' in b ? b.titleInWizard || a.title : a.title;
|
||||
if (labelA < labelB) {
|
||||
return -1;
|
||||
}
|
||||
if (labelA > labelB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.filter(({ disableCreate }: BaseVisType) => !disableCreate);
|
||||
|
||||
const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED);
|
||||
const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS);
|
||||
const legacyVisTypes = getSortedVisTypesByGroup(VisGroups.LEGACY);
|
||||
|
||||
const visTypeAliases = visualizationsService
|
||||
.getAliases()
|
||||
.sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) =>
|
||||
a === b ? 0 : a ? -1 : 1
|
||||
)
|
||||
.filter(({ disableCreate }: VisTypeAlias) => !disableCreate);
|
||||
|
||||
const augmentedCreateNewVisType = useCallback(
|
||||
(visType: Parameters<typeof createNewVisType>[0], cb: () => void) => {
|
||||
const visClickHandler = createNewVisType(visType);
|
||||
return () => {
|
||||
visClickHandler();
|
||||
cb();
|
||||
};
|
||||
},
|
||||
[createNewVisType]
|
||||
);
|
||||
|
||||
const getVisTypeMenuItem = useCallback(
|
||||
(onClickCb: () => void, visType: BaseVisType): PanelSelectionMenuItem => {
|
||||
const {
|
||||
name,
|
||||
title,
|
||||
titleInWizard,
|
||||
description,
|
||||
icon = 'empty',
|
||||
isDeprecated,
|
||||
order,
|
||||
} = visType;
|
||||
return {
|
||||
id: name,
|
||||
name: titleInWizard || title,
|
||||
isDeprecated,
|
||||
icon,
|
||||
onClick: augmentedCreateNewVisType(visType, onClickCb),
|
||||
'data-test-subj': `visType-${name}`,
|
||||
description,
|
||||
order,
|
||||
};
|
||||
},
|
||||
[augmentedCreateNewVisType]
|
||||
);
|
||||
|
||||
const getVisTypeAliasMenuItem = useCallback(
|
||||
(onClickCb: () => void, visTypeAlias: VisTypeAlias): PanelSelectionMenuItem => {
|
||||
const { name, title, description, icon = 'empty', order } = visTypeAlias;
|
||||
|
||||
return {
|
||||
id: name,
|
||||
name: title,
|
||||
icon,
|
||||
onClick: augmentedCreateNewVisType(visTypeAlias, onClickCb),
|
||||
'data-test-subj': `visType-${name}`,
|
||||
description,
|
||||
order: order ?? 0,
|
||||
};
|
||||
},
|
||||
[augmentedCreateNewVisType]
|
||||
);
|
||||
|
||||
const addPanelAction$ = useMemo(
|
||||
() =>
|
||||
defer(() => {
|
||||
return from(
|
||||
uiActionsService.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, {
|
||||
embeddable: api,
|
||||
}) ?? []
|
||||
);
|
||||
}),
|
||||
[api]
|
||||
);
|
||||
|
||||
const computeAvailablePanels = useCallback(
|
||||
(onPanelSelected: () => void) => {
|
||||
if (!panelsComputeSubscription.current) {
|
||||
panelsComputeSubscription.current = addPanelAction$
|
||||
.pipe(
|
||||
map((addPanelActions) =>
|
||||
getAddPanelActionMenuItemsGroup(api, addPanelActions, onPanelSelected)
|
||||
),
|
||||
map((groupedAddPanelAction) => {
|
||||
return sortGroupPanelsByOrder<GroupedAddPanelActions>(
|
||||
Object.values(groupedAddPanelAction)
|
||||
).map((panelGroup) => {
|
||||
switch (panelGroup.id) {
|
||||
case 'visualizations': {
|
||||
return {
|
||||
...panelGroup,
|
||||
items: sortGroupPanelsByOrder<PanelSelectionMenuItem>(
|
||||
(panelGroup.items ?? []).concat(
|
||||
// TODO: actually add grouping to vis type alias so we wouldn't randomly display an unintended item
|
||||
visTypeAliases.map(getVisTypeAliasMenuItem.bind(null, onPanelSelected)),
|
||||
promotedVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected))
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
case ADD_PANEL_LEGACY_GROUP.id: {
|
||||
return {
|
||||
...panelGroup,
|
||||
items: sortGroupPanelsByOrder<PanelSelectionMenuItem>(
|
||||
(panelGroup.items ?? []).concat(
|
||||
legacyVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected))
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
case ADD_PANEL_ANNOTATION_GROUP.id: {
|
||||
return {
|
||||
...panelGroup,
|
||||
items: sortGroupPanelsByOrder<PanelSelectionMenuItem>(
|
||||
(panelGroup.items ?? []).concat(
|
||||
toolVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected))
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
...panelGroup,
|
||||
items: sortGroupPanelsByOrder(panelGroup.items),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe(panelsComputeResultCache.current);
|
||||
}
|
||||
},
|
||||
[
|
||||
api,
|
||||
addPanelAction$,
|
||||
getVisTypeMenuItem,
|
||||
getVisTypeAliasMenuItem,
|
||||
toolVisTypes,
|
||||
legacyVisTypes,
|
||||
promotedVisTypes,
|
||||
visTypeAliases,
|
||||
]
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
(...args: Parameters<typeof computeAvailablePanels>) => {
|
||||
computeAvailablePanels(...args);
|
||||
return lastValueFrom(panelsComputeResultCache.current.asObservable());
|
||||
},
|
||||
[computeAvailablePanels]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { ToolbarButton } from '@kbn/shared-ux-button-toolbar';
|
||||
|
||||
import { AddPanelFlyout } from './add_panel_flyout';
|
||||
import { useDashboardApi } from '../../../../dashboard_api/use_dashboard_api';
|
||||
import { coreServices } from '../../../../services/kibana_services';
|
||||
|
||||
export const AddPanelButton = ({ isDisabled }: { isDisabled?: boolean }) => {
|
||||
const dashboardApi = useDashboardApi();
|
||||
|
||||
useEffect(() => {
|
||||
// ensure opened overlays are closed if a navigation event happens
|
||||
return () => {
|
||||
dashboardApi.clearOverlays();
|
||||
};
|
||||
}, [dashboardApi]);
|
||||
|
||||
const openFlyout = useCallback(() => {
|
||||
const overlayRef = coreServices.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
React.createElement(function () {
|
||||
return <AddPanelFlyout dashboardApi={dashboardApi} />;
|
||||
}),
|
||||
coreServices
|
||||
),
|
||||
{
|
||||
size: 'm',
|
||||
maxWidth: 500,
|
||||
paddingSize: 'm',
|
||||
'aria-labelledby': 'addPanelsFlyout',
|
||||
'data-test-subj': 'dashboardPanelSelectionFlyout',
|
||||
onClose() {
|
||||
dashboardApi.clearOverlays();
|
||||
overlayRef.close();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
dashboardApi.openOverlay(overlayRef);
|
||||
}, [dashboardApi]);
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
data-test-subj="dashboardEditorMenuButton"
|
||||
isDisabled={isDisabled}
|
||||
iconType="plusInCircle"
|
||||
label={i18n.translate('dashboard.solutionToolbar.editorMenuButtonLabel', {
|
||||
defaultMessage: 'Add panel',
|
||||
})}
|
||||
onClick={openFlyout}
|
||||
size="s"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { AddPanelFlyout } from './add_panel_flyout';
|
||||
import { DashboardApi } from '../../../../dashboard_api/types';
|
||||
|
||||
jest.mock('../get_menu_item_groups', () => ({}));
|
||||
|
||||
const mockDashboardApi = {} as unknown as DashboardApi;
|
||||
|
||||
describe('AddPanelFlyout', () => {
|
||||
describe('getMenuItemGroups throws', () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('../get_menu_item_groups').getMenuItemGroups = async () => {
|
||||
throw new Error('simulated getMenuItemGroups error');
|
||||
};
|
||||
});
|
||||
|
||||
test('displays getMenuItemGroups error', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AddPanelFlyout dashboardApi={mockDashboardApi} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByTestId('dashboardPanelSelectionErrorIndicator');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMenuItemGroups returns results', () => {
|
||||
const onClickMock = jest.fn();
|
||||
beforeEach(() => {
|
||||
onClickMock.mockClear();
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('../get_menu_item_groups').getMenuItemGroups = async () => [
|
||||
{
|
||||
id: 'panel1',
|
||||
title: 'App 1',
|
||||
items: [
|
||||
{
|
||||
icon: 'icon1',
|
||||
id: 'mockFactory',
|
||||
name: 'Factory 1',
|
||||
description: 'Factory 1 description',
|
||||
'data-test-subj': 'myItem',
|
||||
onClick: onClickMock,
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
order: 10,
|
||||
'data-test-subj': 'dashboardEditorMenu-group1Group',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
test('calls item onClick handler when item is clicked', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AddPanelFlyout dashboardApi={mockDashboardApi} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByTestId('myItem'));
|
||||
expect(onClickMock).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('displays not found message when a user searches for an item that is not in the selection list', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AddPanelFlyout dashboardApi={mockDashboardApi} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.type(
|
||||
screen.getByTestId('dashboardPanelSelectionFlyout__searchInput'),
|
||||
'non existent panel'
|
||||
);
|
||||
screen.getByTestId('dashboardPanelSelectionNoPanelMessage');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { i18n as i18nFn } from '@kbn/i18n';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiTitle,
|
||||
EuiFieldSearch,
|
||||
useEuiTheme,
|
||||
EuiSkeletonText,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { DashboardApi } from '../../../../dashboard_api/types';
|
||||
import { MenuItem, MenuItemGroup } from '../types';
|
||||
import { getMenuItemGroups } from '../get_menu_item_groups';
|
||||
import { Groups } from './groups';
|
||||
|
||||
export function AddPanelFlyout({ dashboardApi }: { dashboardApi: DashboardApi }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const {
|
||||
value: groups,
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
return await getMenuItemGroups(dashboardApi);
|
||||
}, [dashboardApi]);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [filteredGroups, setFilteredGroups] = useState<MenuItemGroup[]>([]);
|
||||
useEffect(() => {
|
||||
if (!searchTerm) {
|
||||
return setFilteredGroups(groups ?? []);
|
||||
}
|
||||
|
||||
const q = searchTerm.toLowerCase();
|
||||
|
||||
const currentGroups = groups ?? ([] as MenuItemGroup[]);
|
||||
setFilteredGroups(
|
||||
currentGroups
|
||||
.map((group) => {
|
||||
const groupMatch = group.title.toLowerCase().includes(q);
|
||||
|
||||
const [itemsMatch, items] = group.items.reduce(
|
||||
(acc, item) => {
|
||||
const itemMatch = item.name.toLowerCase().includes(q);
|
||||
|
||||
acc[0] = acc[0] || itemMatch;
|
||||
acc[1].push({
|
||||
...item,
|
||||
isDisabled: !(groupMatch || itemMatch),
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
[false, [] as MenuItem[]]
|
||||
);
|
||||
|
||||
return {
|
||||
...group,
|
||||
isDisabled: !(groupMatch || itemsMatch),
|
||||
items,
|
||||
};
|
||||
})
|
||||
.filter((group) => !group.isDisabled)
|
||||
);
|
||||
}, [groups, searchTerm]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h1 id="addPanelsFlyout">
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.headingText"
|
||||
defaultMessage="Add panel"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiSkeletonText isLoading={loading}>
|
||||
<EuiFlexGroup direction="column" responsive={false} gutterSize="m">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={{
|
||||
position: 'sticky',
|
||||
top: euiTheme.size.m,
|
||||
zIndex: 1,
|
||||
boxShadow: `0 -${euiTheme.size.m} 0 4px ${euiTheme.colors.emptyShade}`,
|
||||
}}
|
||||
>
|
||||
<EuiForm component="form" fullWidth>
|
||||
<EuiFormRow css={{ backgroundColor: euiTheme.colors.emptyShade }}>
|
||||
<EuiFieldSearch
|
||||
compressed
|
||||
autoFocus
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
aria-label={i18nFn.translate(
|
||||
'dashboard.editorMenu.addPanelFlyout.searchLabelText',
|
||||
{ defaultMessage: 'search field for panels' }
|
||||
)}
|
||||
data-test-subj="dashboardPanelSelectionFlyout__searchInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={{
|
||||
minHeight: '20vh',
|
||||
...(error
|
||||
? {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{error ? (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
iconColor="danger"
|
||||
body={
|
||||
<EuiText size="s" textAlign="center">
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.loadingErrorDescription"
|
||||
defaultMessage="An error occurred loading the available dashboard panels for selection"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
data-test-subj="dashboardPanelSelectionErrorIndicator"
|
||||
/>
|
||||
) : (
|
||||
<Groups groups={filteredGroups} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSkeletonText>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={dashboardApi.clearOverlays}
|
||||
data-test-subj="dashboardPanelSelectionCloseBtn"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.cancelButtonText"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiListGroup,
|
||||
EuiListGroupItem,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { MenuItemGroup } from '../types';
|
||||
|
||||
export function Group({ group }: { group: MenuItemGroup }) {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle id={`${group.id}-group`} size="xxs">
|
||||
<h3>{group.title}</h3>
|
||||
</EuiTitle>
|
||||
<EuiListGroup
|
||||
aria-labelledby={`${group.id}-group`}
|
||||
size="s"
|
||||
gutterSize="none"
|
||||
maxWidth={false}
|
||||
flush
|
||||
>
|
||||
{group.items.map((item) => {
|
||||
return (
|
||||
<EuiListGroupItem
|
||||
key={item.id}
|
||||
label={
|
||||
<EuiToolTip position="right" content={item.description}>
|
||||
{!item.isDeprecated ? (
|
||||
<EuiText size="s">{item.name}</EuiText>
|
||||
) : (
|
||||
<EuiFlexGroup wrap responsive={false} gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{item.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="warning">
|
||||
<FormattedMessage
|
||||
id="dashboard.editorMenu.deprecatedTag"
|
||||
defaultMessage="Deprecated"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiToolTip>
|
||||
}
|
||||
onClick={item.onClick}
|
||||
iconType={item.icon}
|
||||
data-test-subj={item['data-test-subj']}
|
||||
isDisabled={item.isDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiListGroup>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Group } from './group';
|
||||
import { MenuItemGroup } from '../types';
|
||||
|
||||
export function Groups({ groups }: { groups: MenuItemGroup[] }) {
|
||||
return groups.length === 0 ? (
|
||||
<EuiText size="s" textAlign="center" data-test-subj="dashboardPanelSelectionNoPanelMessage">
|
||||
<FormattedMessage
|
||||
id="dashboard.solutionToolbar.addPanelFlyout.noResultsDescription"
|
||||
defaultMessage="No panel types found"
|
||||
/>
|
||||
</EuiText>
|
||||
) : (
|
||||
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="dashboardPanelSelectionList">
|
||||
{groups.map((group) => (
|
||||
<EuiFlexItem
|
||||
key={group.id}
|
||||
data-test-subj={group['data-test-subj']}
|
||||
data-group-sort-order={group.order}
|
||||
>
|
||||
<Group group={group} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { getMenuItemGroups } from './get_menu_item_groups';
|
||||
|
||||
jest.mock('../../../services/kibana_services', () => ({
|
||||
uiActionsService: {
|
||||
getTriggerCompatibleActions: async () => [
|
||||
{
|
||||
id: 'mockAddPanelAction',
|
||||
type: '',
|
||||
order: 10,
|
||||
grouping: [
|
||||
{
|
||||
id: 'myGroup',
|
||||
order: 900,
|
||||
getDisplayName: () => 'My group',
|
||||
},
|
||||
],
|
||||
getDisplayName: () => 'mockAddPanelAction',
|
||||
getIconType: () => 'empty',
|
||||
execute: () => {},
|
||||
isCompatible: async () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
visualizationsService: {
|
||||
all: () => [
|
||||
{
|
||||
name: 'myPromotedVis',
|
||||
title: 'myPromotedVis',
|
||||
order: 0,
|
||||
description: 'myPromotedVis description',
|
||||
icon: 'empty',
|
||||
stage: 'production',
|
||||
isDeprecated: false,
|
||||
group: 'promoted',
|
||||
titleInWizard: 'myPromotedVis title',
|
||||
},
|
||||
],
|
||||
getAliases: () => [
|
||||
{
|
||||
name: 'alias visualization',
|
||||
title: 'Alias Visualization',
|
||||
order: 0,
|
||||
description: 'This is a dummy representation of aan aliased visualization.',
|
||||
icon: 'empty',
|
||||
stage: 'production',
|
||||
isDeprecated: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
|
||||
describe('getMenuItemGroups', () => {
|
||||
test('gets sorted groups from visTypes, visTypeAliases, and add panel actions', async () => {
|
||||
const api = {
|
||||
getAppContext: () => ({
|
||||
currentAppId: 'dashboards',
|
||||
}),
|
||||
openOverlay: () => {},
|
||||
clearOverlays: () => {},
|
||||
};
|
||||
const groups = await getMenuItemGroups(api);
|
||||
expect(groups.length).toBe(2);
|
||||
|
||||
expect(groups[0].title).toBe('Visualizations');
|
||||
expect(groups[0].items.length).toBe(2); // promoted vis type and vis alias
|
||||
|
||||
expect(groups[1].title).toBe('My group');
|
||||
expect(groups[1].items.length).toBe(1); // add panel action
|
||||
});
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { VisGroups } from '@kbn/visualizations-plugin/public';
|
||||
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
ADD_PANEL_ANNOTATION_GROUP,
|
||||
ADD_PANEL_LEGACY_GROUP,
|
||||
ADD_PANEL_OTHER_GROUP,
|
||||
ADD_PANEL_VISUALIZATION_GROUP,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { TracksOverlays } from '@kbn/presentation-containers';
|
||||
import { PresentableGroup } from '@kbn/ui-actions-browser/src/types';
|
||||
import { addPanelMenuTrigger } from '@kbn/ui-actions-plugin/public';
|
||||
import type { HasAppContext } from '@kbn/presentation-publishing';
|
||||
import { uiActionsService, visualizationsService } from '../../../services/kibana_services';
|
||||
import { navigateToVisEditor } from './navigate_to_vis_editor';
|
||||
import type { MenuItem, MenuItemGroup } from './types';
|
||||
|
||||
const VIS_GROUP_TO_ADD_PANEL_GROUP: Record<VisGroups, undefined | PresentableGroup> = {
|
||||
[VisGroups.AGGBASED]: undefined,
|
||||
[VisGroups.PROMOTED]: ADD_PANEL_VISUALIZATION_GROUP,
|
||||
[VisGroups.TOOLS]: ADD_PANEL_ANNOTATION_GROUP,
|
||||
[VisGroups.LEGACY]: ADD_PANEL_LEGACY_GROUP,
|
||||
};
|
||||
|
||||
export async function getMenuItemGroups(
|
||||
api: HasAppContext & TracksOverlays
|
||||
): Promise<MenuItemGroup[]> {
|
||||
const groups: Record<string, MenuItemGroup> = {};
|
||||
const addPanelContext = {
|
||||
embeddable: api,
|
||||
trigger: addPanelMenuTrigger,
|
||||
};
|
||||
function pushItem(group: PresentableGroup, item: MenuItem) {
|
||||
if (!groups[group.id]) {
|
||||
groups[group.id] = {
|
||||
id: group.id,
|
||||
title: group.getDisplayName?.(addPanelContext) ?? '',
|
||||
'data-test-subj': `dashboardEditorMenu-${group.id}Group`,
|
||||
order: group.order ?? 0,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
groups[group.id].items.push(item);
|
||||
}
|
||||
|
||||
// add menu items from vis types
|
||||
visualizationsService.all().forEach((visType) => {
|
||||
if (visType.disableCreate) return;
|
||||
|
||||
const group = VIS_GROUP_TO_ADD_PANEL_GROUP[visType.group];
|
||||
if (!group) return;
|
||||
pushItem(group, {
|
||||
id: visType.name,
|
||||
name: visType.titleInWizard || visType.title,
|
||||
isDeprecated: visType.isDeprecated,
|
||||
icon: visType.icon ?? 'empty',
|
||||
onClick: () => {
|
||||
api.clearOverlays();
|
||||
navigateToVisEditor(api, visType);
|
||||
},
|
||||
'data-test-subj': `visType-${visType.name}`,
|
||||
description: visType.description,
|
||||
order: visType.order,
|
||||
});
|
||||
});
|
||||
|
||||
// add menu items from vis alias
|
||||
visualizationsService.getAliases().forEach((visTypeAlias) => {
|
||||
if (visTypeAlias.disableCreate) return;
|
||||
pushItem(ADD_PANEL_VISUALIZATION_GROUP, {
|
||||
id: visTypeAlias.name,
|
||||
name: visTypeAlias.title,
|
||||
icon: visTypeAlias.icon ?? 'empty',
|
||||
onClick: () => {
|
||||
api.clearOverlays();
|
||||
navigateToVisEditor(api, visTypeAlias);
|
||||
},
|
||||
'data-test-subj': `visType-${visTypeAlias.name}`,
|
||||
description: visTypeAlias.description,
|
||||
order: visTypeAlias.order ?? 0,
|
||||
});
|
||||
});
|
||||
|
||||
// add menu items from "add panel" actions
|
||||
(
|
||||
await uiActionsService.getTriggerCompatibleActions(ADD_PANEL_TRIGGER, { embeddable: api })
|
||||
).forEach((action) => {
|
||||
const actionGroups = Array.isArray(action.grouping) ? action.grouping : [ADD_PANEL_OTHER_GROUP];
|
||||
actionGroups.forEach((group) => {
|
||||
const actionName = action.getDisplayName(addPanelContext);
|
||||
pushItem(group, {
|
||||
id: action.id,
|
||||
name: actionName,
|
||||
icon: action.getIconType?.(addPanelContext) ?? 'empty',
|
||||
onClick: (event: React.MouseEvent) => {
|
||||
api.clearOverlays();
|
||||
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(addPanelContext);
|
||||
},
|
||||
'data-test-subj': `create-action-${actionName}`,
|
||||
description: action?.getDisplayNameTooltip?.(addPanelContext),
|
||||
order: action.order ?? 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(groups)
|
||||
.map((group) => {
|
||||
group.items.sort((itemA, itemB) => {
|
||||
return itemA.order === itemB.order
|
||||
? itemA.name.localeCompare(itemB.name)
|
||||
: itemB.order - itemA.order;
|
||||
});
|
||||
return group;
|
||||
})
|
||||
.sort((groupA, groupB) => groupB.order - groupA.order);
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { HasAppContext } from '@kbn/presentation-publishing';
|
||||
import type { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import {
|
||||
dataService,
|
||||
embeddableService,
|
||||
usageCollectionService,
|
||||
} from '../../../services/kibana_services';
|
||||
import { DASHBOARD_UI_METRIC_ID } from '../../../utils/telemetry_constants';
|
||||
|
||||
export function navigateToVisEditor(api: HasAppContext, visType?: BaseVisType | VisTypeAlias) {
|
||||
let path = '';
|
||||
let appId = '';
|
||||
|
||||
if (visType) {
|
||||
const trackUiMetric = usageCollectionService?.reportUiCounter.bind(
|
||||
usageCollectionService,
|
||||
DASHBOARD_UI_METRIC_ID
|
||||
);
|
||||
if (trackUiMetric) {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`);
|
||||
}
|
||||
|
||||
if (!('alias' in visType)) {
|
||||
// this visualization is not an alias
|
||||
appId = 'visualize';
|
||||
path = `#/create?type=${encodeURIComponent(visType.name)}`;
|
||||
} else if (visType.alias && 'path' in visType.alias) {
|
||||
// this visualization **is** an alias, and it has an app to redirect to for creation
|
||||
appId = visType.alias.app;
|
||||
path = visType.alias.path;
|
||||
}
|
||||
} else {
|
||||
appId = 'visualize';
|
||||
path = '#/create?';
|
||||
}
|
||||
|
||||
const stateTransferService = embeddableService.getStateTransfer();
|
||||
stateTransferService.navigateToEditor(appId, {
|
||||
path,
|
||||
state: {
|
||||
originatingApp: api.getAppContext()?.currentAppId,
|
||||
originatingPath: api.getAppContext()?.getCurrentPath?.(),
|
||||
searchSessionId: dataService.search.session.getSessionId(),
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import type { IconType, CommonProps } from '@elastic/eui';
|
||||
|
||||
export interface MenuItem extends Pick<CommonProps, 'data-test-subj'> {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: IconType;
|
||||
onClick: MouseEventHandler;
|
||||
description?: string;
|
||||
isDisabled?: boolean;
|
||||
isDeprecated?: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface MenuItemGroup extends Pick<CommonProps, 'data-test-subj'> {
|
||||
id: string;
|
||||
isDisabled?: boolean;
|
||||
title: string;
|
||||
order: number;
|
||||
items: MenuItem[];
|
||||
}
|
|
@ -9,80 +9,32 @@
|
|||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AddFromLibraryButton, Toolbar, ToolbarButton } from '@kbn/shared-ux-button-toolbar';
|
||||
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
|
||||
import { DASHBOARD_UI_METRIC_ID } from '../../utils/telemetry_constants';
|
||||
import {
|
||||
dataService,
|
||||
embeddableService,
|
||||
usageCollectionService,
|
||||
visualizationsService,
|
||||
} from '../../services/kibana_services';
|
||||
import { visualizationsService } from '../../services/kibana_services';
|
||||
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
|
||||
import { ControlsToolbarButton } from './controls_toolbar_button';
|
||||
import { EditorMenu } from './editor_menu';
|
||||
import { AddPanelButton } from './add_panel_button/components/add_panel_button';
|
||||
import { addFromLibrary } from '../../dashboard_container/embeddable/api';
|
||||
import { navigateToVisEditor } from './add_panel_button/navigate_to_vis_editor';
|
||||
|
||||
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const dashboardApi = useDashboardApi();
|
||||
|
||||
const lensAlias = useMemo(
|
||||
() => visualizationsService.getAliases().find(({ name }) => name === 'lens'),
|
||||
[]
|
||||
);
|
||||
|
||||
const createNewVisType = useCallback(
|
||||
(visType?: BaseVisType | VisTypeAlias) => () => {
|
||||
let path = '';
|
||||
let appId = '';
|
||||
|
||||
if (visType) {
|
||||
const trackUiMetric = usageCollectionService?.reportUiCounter.bind(
|
||||
usageCollectionService,
|
||||
DASHBOARD_UI_METRIC_ID
|
||||
);
|
||||
if (trackUiMetric) {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`);
|
||||
}
|
||||
|
||||
if (!('alias' in visType)) {
|
||||
// this visualization is not an alias
|
||||
appId = 'visualize';
|
||||
path = `#/create?type=${encodeURIComponent(visType.name)}`;
|
||||
} else if (visType.alias && 'path' in visType.alias) {
|
||||
// this visualization **is** an alias, and it has an app to redirect to for creation
|
||||
appId = visType.alias.app;
|
||||
path = visType.alias.path;
|
||||
}
|
||||
} else {
|
||||
appId = 'visualize';
|
||||
path = '#/create?';
|
||||
}
|
||||
|
||||
const stateTransferService = embeddableService.getStateTransfer();
|
||||
stateTransferService.navigateToEditor(appId, {
|
||||
path,
|
||||
state: {
|
||||
originatingApp: dashboardApi.getAppContext()?.currentAppId,
|
||||
originatingPath: dashboardApi.getAppContext()?.getCurrentPath?.(),
|
||||
searchSessionId: dataService.search.session.getSessionId(),
|
||||
},
|
||||
});
|
||||
},
|
||||
[dashboardApi]
|
||||
);
|
||||
const navigateToDefaultEditor = useCallback(() => {
|
||||
const lensAlias = visualizationsService.getAliases().find(({ name }) => name === 'lens');
|
||||
navigateToVisEditor(dashboardApi, lensAlias);
|
||||
}, [dashboardApi]);
|
||||
|
||||
const controlGroupApi = useStateFromPublishingSubject(dashboardApi.controlGroupApi$);
|
||||
const extraButtons = [
|
||||
<EditorMenu createNewVisType={createNewVisType} isDisabled={isDisabled} />,
|
||||
<AddPanelButton isDisabled={isDisabled} />,
|
||||
<AddFromLibraryButton
|
||||
onClick={() => addFromLibrary(dashboardApi)}
|
||||
size="s"
|
||||
|
@ -106,7 +58,7 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
|
|||
isDisabled={isDisabled}
|
||||
iconType="lensApp"
|
||||
size="s"
|
||||
onClick={createNewVisType(lensAlias)}
|
||||
onClick={navigateToDefaultEditor}
|
||||
label={getCreateVisualizationButtonTitle()}
|
||||
data-test-subj="dashboardAddNewPanelButton"
|
||||
/>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
.dshSolutionToolbar__editorContextMenu {
|
||||
max-height: 60vh;
|
||||
overflow-y: scroll;
|
||||
@include euiScrollBar;
|
||||
@include euiOverflowShadow;
|
||||
}
|
|
@ -1,31 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { buildMockDashboardApi } from '../../mocks';
|
||||
import { EditorMenu } from './editor_menu';
|
||||
|
||||
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
|
||||
import { uiActionsService, visualizationsService } from '../../services/kibana_services';
|
||||
|
||||
jest.spyOn(uiActionsService, 'getTriggerCompatibleActions').mockResolvedValue([]);
|
||||
jest.spyOn(visualizationsService, 'getByGroup').mockReturnValue([]);
|
||||
jest.spyOn(visualizationsService, 'getAliases').mockReturnValue([]);
|
||||
|
||||
describe('editor menu', () => {
|
||||
it('renders without crashing', async () => {
|
||||
const { api } = buildMockDashboardApi();
|
||||
render(<EditorMenu createNewVisType={jest.fn()} />, {
|
||||
wrapper: ({ children }) => {
|
||||
return <DashboardContext.Provider value={api}>{children}</DashboardContext.Provider>;
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,92 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './editor_menu.scss';
|
||||
|
||||
import React, { useEffect, useCallback, type ComponentProps } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { ToolbarButton } from '@kbn/shared-ux-button-toolbar';
|
||||
|
||||
import { useGetDashboardPanels, DashboardPanelSelectionListFlyout } from './add_new_panel';
|
||||
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
|
||||
import { coreServices } from '../../services/kibana_services';
|
||||
|
||||
interface EditorMenuProps
|
||||
extends Pick<Parameters<typeof useGetDashboardPanels>[0], 'createNewVisType'> {
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) => {
|
||||
const dashboardApi = useDashboardApi();
|
||||
|
||||
const fetchDashboardPanels = useGetDashboardPanels({
|
||||
api: dashboardApi,
|
||||
createNewVisType,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// ensure opened dashboard is closed if a navigation event happens;
|
||||
return () => {
|
||||
dashboardApi.clearOverlays();
|
||||
};
|
||||
}, [dashboardApi]);
|
||||
|
||||
const openDashboardPanelSelectionFlyout = useCallback(
|
||||
function openDashboardPanelSelectionFlyout() {
|
||||
const flyoutPanelPaddingSize: ComponentProps<
|
||||
typeof DashboardPanelSelectionListFlyout
|
||||
>['paddingSize'] = 'm';
|
||||
|
||||
const mount = toMountPoint(
|
||||
React.createElement(function () {
|
||||
return (
|
||||
<DashboardPanelSelectionListFlyout
|
||||
close={dashboardApi.clearOverlays}
|
||||
{...{
|
||||
paddingSize: flyoutPanelPaddingSize,
|
||||
fetchDashboardPanels: fetchDashboardPanels.bind(null, dashboardApi.clearOverlays),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
coreServices
|
||||
);
|
||||
|
||||
dashboardApi.openOverlay(
|
||||
coreServices.overlays.openFlyout(mount, {
|
||||
size: 'm',
|
||||
maxWidth: 500,
|
||||
paddingSize: flyoutPanelPaddingSize,
|
||||
'aria-labelledby': 'addPanelsFlyout',
|
||||
'data-test-subj': 'dashboardPanelSelectionFlyout',
|
||||
onClose(overlayRef) {
|
||||
dashboardApi.clearOverlays();
|
||||
overlayRef.close();
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[dashboardApi, fetchDashboardPanels]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
data-test-subj="dashboardEditorMenuButton"
|
||||
isDisabled={isDisabled}
|
||||
iconType="plusInCircle"
|
||||
label={i18n.translate('dashboard.solutionToolbar.editorMenuButtonLabel', {
|
||||
defaultMessage: 'Add panel',
|
||||
})}
|
||||
onClick={openDashboardPanelSelectionFlyout}
|
||||
size="s"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -85,7 +85,8 @@
|
|||
"@kbn/core-rendering-browser",
|
||||
"@kbn/esql-variables-types",
|
||||
"@kbn/grid-layout",
|
||||
"@kbn/esql-validation-autocomplete"
|
||||
"@kbn/esql-validation-autocomplete",
|
||||
"@kbn/ui-actions-browser"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -57,4 +57,5 @@ export {
|
|||
ADD_PANEL_ANNOTATION_GROUP,
|
||||
ADD_PANEL_OTHER_GROUP,
|
||||
ADD_PANEL_LEGACY_GROUP,
|
||||
ADD_PANEL_VISUALIZATION_GROUP,
|
||||
} from './ui_actions/add_panel_groups';
|
||||
|
|
|
@ -9,6 +9,18 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ADD_PANEL_VISUALIZATION_GROUP = {
|
||||
id: 'visualizations',
|
||||
getDisplayName: () =>
|
||||
i18n.translate('embeddableApi.common.constants.grouping.visualizations', {
|
||||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
getIconType: () => {
|
||||
return 'visGauge';
|
||||
},
|
||||
order: 1000,
|
||||
};
|
||||
|
||||
export const ADD_PANEL_ANNOTATION_GROUP = {
|
||||
id: 'annotation-and-navigation',
|
||||
getDisplayName: () =>
|
||||
|
|
|
@ -20,7 +20,6 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
export { TypesService } from './vis_types/types_service';
|
||||
export { VIS_EVENT_TO_TRIGGER } from './embeddable/events';
|
||||
export { apiHasVisualizeConfig } from './embeddable/interfaces/has_visualize_config';
|
||||
export { COMMON_VISUALIZATION_GROUPING } from './legacy/embeddable/constants';
|
||||
export { VisualizationContainer } from './components';
|
||||
export { getVisSchemas } from './vis_schemas';
|
||||
export { prepareLogTable } from '../common/utils/prepare_log_table';
|
||||
|
|
|
@ -1,26 +0,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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../common/constants';
|
||||
|
||||
export const COMMON_VISUALIZATION_GROUPING = [
|
||||
{
|
||||
id: 'visualizations',
|
||||
getDisplayName: () =>
|
||||
i18n.translate('visualizations.common.constants.grouping.legacy', {
|
||||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
getIconType: () => {
|
||||
return 'visGauge';
|
||||
},
|
||||
order: 1000,
|
||||
},
|
||||
];
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { VISUALIZE_EMBEDDABLE_TYPE, COMMON_VISUALIZATION_GROUPING } from './constants';
|
||||
export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../common/constants';
|
||||
export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object';
|
||||
|
||||
export type { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable';
|
||||
|
|
|
@ -38,7 +38,7 @@ import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public';
|
|||
import { isFallbackDataView } from '../../visualize_app/utils';
|
||||
import { VisualizationMissedSavedObjectError } from '../../components/visualization_missed_saved_object_error';
|
||||
import VisualizationError from '../../components/visualization_error';
|
||||
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
|
||||
import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../common/constants';
|
||||
import { SerializedVis, Vis } from '../../vis';
|
||||
import { getApplication, getExpressions, getUiActions } from '../../services';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '../../embeddable/events';
|
||||
|
|
|
@ -9531,7 +9531,6 @@
|
|||
"visualizations.badge.readOnly.text": "Lecture seule",
|
||||
"visualizations.badge.readOnly.tooltip": "Impossible d'enregistrer les visualisations dans la bibliothèque",
|
||||
"visualizations.byValue_pageHeading": "Visualisation de type {chartType} intégrée à l'application {originatingApp}",
|
||||
"visualizations.common.constants.grouping.legacy": "Visualisations",
|
||||
"visualizations.confirmModal.cancelButtonLabel": "Annuler",
|
||||
"visualizations.confirmModal.confirmTextDescription": "Quitter l'éditeur Visualize sans enregistrer les modifications ?",
|
||||
"visualizations.confirmModal.overwriteButtonLabel": "Écraser",
|
||||
|
|
|
@ -9406,7 +9406,6 @@
|
|||
"visualizations.badge.readOnly.text": "読み取り専用",
|
||||
"visualizations.badge.readOnly.tooltip": "ビジュアライゼーションをライブラリに保存できません",
|
||||
"visualizations.byValue_pageHeading": "{originatingApp}アプリに埋め込まれた{chartType}タイプのビジュアライゼーション",
|
||||
"visualizations.common.constants.grouping.legacy": "ビジュアライゼーション",
|
||||
"visualizations.confirmModal.cancelButtonLabel": "キャンセル",
|
||||
"visualizations.confirmModal.confirmTextDescription": "変更を保存せずにVisualizeエディターから移動しますか?",
|
||||
"visualizations.confirmModal.overwriteButtonLabel": "上書き",
|
||||
|
|
|
@ -9265,7 +9265,6 @@
|
|||
"visualizations.badge.readOnly.text": "只读",
|
||||
"visualizations.badge.readOnly.tooltip": "无法将可视化保存到库",
|
||||
"visualizations.byValue_pageHeading": "已嵌入到 {originatingApp} 应用中的 {chartType} 类型可视化",
|
||||
"visualizations.common.constants.grouping.legacy": "可视化",
|
||||
"visualizations.confirmModal.cancelButtonLabel": "取消",
|
||||
"visualizations.confirmModal.confirmTextDescription": "离开 Visualize 编辑器而不保存更改?",
|
||||
"visualizations.confirmModal.overwriteButtonLabel": "覆盖",
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { CoreStart } from '@kbn/core/public';
|
|||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
|
||||
import { COMMON_VISUALIZATION_GROUPING } from '@kbn/visualizations-plugin/public';
|
||||
import { ADD_PANEL_VISUALIZATION_GROUP } from '@kbn/embeddable-plugin/public';
|
||||
import type { LensPluginStartDependencies } from '../../plugin';
|
||||
import type { EditorFrameService } from '../../editor_frame_service';
|
||||
|
||||
|
@ -22,7 +22,7 @@ export class CreateESQLPanelAction implements Action<EmbeddableApiContext> {
|
|||
public id = ACTION_CREATE_ESQL_CHART;
|
||||
public order = 50;
|
||||
|
||||
public grouping = COMMON_VISUALIZATION_GROUPING;
|
||||
public grouping = [ADD_PANEL_VISUALIZATION_GROUP];
|
||||
|
||||
constructor(
|
||||
protected readonly startDependencies: LensPluginStartDependencies,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue