diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index 0320daa2ec338..ac254714adbb7 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -45,9 +45,9 @@ jest.mock('./utils', () => ({ getCustomChartData: jest.fn().mockReturnValue(true), })); -const mockUseVisualizationResponse = jest.fn(() => [ - { aggregations: [{ buckets: [{ key: '1234' }] }], hits: { total: 999 } }, -]); +const mockUseVisualizationResponse = jest.fn(() => ({ + responses: [{ aggregations: [{ buckets: [{ key: '1234' }] }], hits: { total: 999 } }], +})); jest.mock('../visualization_actions/use_visualization_response', () => ({ useVisualizationResponse: () => mockUseVisualizationResponse(), })); @@ -345,9 +345,9 @@ describe('Matrix Histogram Component', () => { }); test('it should render 0 as subtitle when buckets are empty', () => { - mockUseVisualizationResponse.mockReturnValue([ - { aggregations: [{ buckets: [] }], hits: { total: 999 } }, - ]); + mockUseVisualizationResponse.mockReturnValue({ + responses: [{ aggregations: [{ buckets: [] }], hits: { total: 999 } }], + }); mockUseMatrix.mockReturnValue([ false, { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/__mocks__/use_actions.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/__mocks__/use_actions.ts new file mode 100644 index 0000000000000..9f5f46fb67f68 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/__mocks__/use_actions.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const VISUALIZATION_CONTEXT_MENU_TRIGGER = 'VISUALIZATION_CONTEXT_MENU_TRIGGER'; +export const DEFAULT_ACTIONS = [ + 'inspect', + 'addToNewCase', + 'addToExistingCase', + 'saveToLibrary', + 'openInLens', +]; +export const MOCK_ACTIONS = [ + { + id: 'inspect', + getDisplayName: () => 'Inspect', + getIconType: () => 'inspect', + type: 'actionButton', + order: 4, + isCompatible: () => true, + execute: jest.fn(), + }, + { + id: 'addToNewCase', + getDisplayName: () => 'Add to new case', + getIconType: () => 'casesApp', + type: 'actionButton', + order: 3, + isCompatible: () => true, + execute: jest.fn(), + }, + { + id: 'addToExistingCase', + getDisplayName: () => 'Add to existing case', + getIconType: () => 'casesApp', + type: 'actionButton', + order: 2, + isCompatible: () => true, + execute: jest.fn(), + }, + { + id: 'saveToLibrary', + getDisplayName: () => 'Added to library', + getIconType: () => 'save', + type: 'actionButton', + order: 1, + isCompatible: () => true, + execute: jest.fn(), + }, + { + id: 'openInLens', + getDisplayName: () => 'Open in Lens', + getIconType: () => 'visArea', + type: 'actionButton', + order: 0, + isCompatible: () => true, + execute: jest.fn(), + }, +]; +export const useActions = jest.fn().mockReturnValue(MOCK_ACTIONS); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx index 924b1158593a7..b3fd18989991c 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx @@ -5,66 +5,36 @@ * 2.0. */ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; -import type { Action } from '@kbn/ui-actions-plugin/public'; +import { EuiContextMenu } from '@elastic/eui'; + +import { fireEvent, render, waitFor } from '@testing-library/react'; import { getDnsTopDomainsLensAttributes } from './lens_attributes/network/dns_top_domains'; import { VisualizationActions } from './actions'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, - TestProviders, -} from '../../mock'; -import type { State } from '../../store'; -import { createStore } from '../../store'; -import type { UpdateQueryParams } from '../../store/inputs/helpers'; -import { upsertQuery } from '../../store/inputs/helpers'; -import { cloneDeep } from 'lodash'; -import { useKibana } from '../../lib/kibana/kibana_react'; -import { CASES_FEATURE_ID } from '../../../../common/constants'; -import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; -import { allCasesCapabilities, allCasesPermissions } from '../../../cases_test_utils'; -import { InputsModelId } from '../../store/inputs/constants'; +import { TestProviders } from '../../mock'; + import type { VisualizationActionsProps } from './types'; import * as useLensAttributesModule from './use_lens_attributes'; import { SourcererScopeName } from '../../store/sourcerer/model'; -jest.mock('react-router-dom', () => { - const actual = jest.requireActual('react-router-dom'); +jest.mock('./use_actions'); + +jest.mock('../inspect/use_inspect', () => { return { - ...actual, - useLocation: jest.fn(() => { - return { pathname: 'network' }; - }), + useInspect: jest.fn().mockReturnValue({}), }; }); -jest.mock('../../lib/kibana/kibana_react'); -jest.mock('../../utils/route/use_route_spy', () => { + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); return { - useRouteSpy: jest.fn(() => [{ pageName: 'network', detailName: '', tabName: 'dns' }]), + ...original, + EuiContextMenu: jest.fn().mockReturnValue(
), }; }); describe('VisualizationActions', () => { - const refetch = jest.fn(); - const state: State = mockGlobalState; - const { storage } = createSecuritySolutionStorageMock(); - const newQuery: UpdateQueryParams = { - inputId: InputsModelId.global, - id: 'networkDnsHistogramQuery', - inspect: { - dsl: ['mockDsl'], - response: ['mockResponse'], - }, - loading: false, - refetch, - state: state.inputs, - }; const spyUseLensAttributes = jest.spyOn(useLensAttributesModule, 'useLensAttributes'); - - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const props: VisualizationActionsProps = { getLensAttributes: getDnsTopDomainsLensAttributes, queryId: 'networkDnsHistogramQuery', @@ -76,64 +46,15 @@ describe('VisualizationActions', () => { extraOptions: { dnsIsPtrIncluded: true }, stackByField: 'dns.question.registered_domain', }; - const mockNavigateToPrefilledEditor = jest.fn(); - const mockGetCreateCaseFlyoutOpen = jest.fn(); - const mockGetAllCasesSelectorModalOpen = jest.fn(); + const mockContextMenu = EuiContextMenu as unknown as jest.Mock; beforeEach(() => { jest.clearAllMocks(); - const cases = mockCasesContract(); - cases.helpers.getUICapabilities.mockReturnValue(allCasesPermissions()); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - lens: { - canUseEditor: jest.fn(() => true), - navigateToPrefilledEditor: mockNavigateToPrefilledEditor, - }, - cases: { - ...mockCasesContract(), - hooks: { - useCasesAddToExistingCaseModal: jest - .fn() - .mockReturnValue({ open: mockGetAllCasesSelectorModalOpen }), - useCasesAddToNewCaseFlyout: jest - .fn() - .mockReturnValue({ open: mockGetCreateCaseFlyoutOpen }), - }, - helpers: { canUseCases: jest.fn().mockReturnValue(allCasesPermissions()) }, - }, - application: { - capabilities: { [CASES_FEATURE_ID]: allCasesCapabilities() }, - getUrlForApp: jest.fn(), - navigateToApp: jest.fn(), - }, - notifications: { - toasts: { - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - remove: jest.fn(), - }, - }, - http: jest.fn(), - data: { - search: jest.fn(), - }, - storage: { - set: jest.fn(), - }, - theme: {}, - }, - }); - const myState = cloneDeep(state); - myState.inputs = upsertQuery(newQuery); - store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); test('Should generate attributes', () => { render( - + ); @@ -150,161 +71,38 @@ describe('VisualizationActions', () => { ); }); - test('Should render VisualizationActions button', () => { - const { container } = render( - - - - ); - expect( - container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`) - ).toBeInTheDocument(); - }); - - test('Should render Open in Lens button', () => { - const { container } = render( - - - - ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - - expect(screen.getByText('Open in Lens')).toBeInTheDocument(); - expect(screen.getByText('Open in Lens')).not.toBeDisabled(); - }); - - test('Should call NavigateToPrefilledEditor when Open in Lens', () => { - const { container } = render( - - - - ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - - fireEvent.click(screen.getByText('Open in Lens')); - expect(mockNavigateToPrefilledEditor.mock.calls[0][0].timeRange).toEqual(props.timerange); - expect(mockNavigateToPrefilledEditor.mock.calls[0][0].attributes.title).toEqual(''); - expect(mockNavigateToPrefilledEditor.mock.calls[0][0].attributes.references).toEqual([ - { - id: 'security-solution', - name: 'indexpattern-datasource-layer-b1c3efc6-c886-4fba-978f-3b6bb5e7948a', - type: 'index-pattern', - }, - ]); - expect(mockNavigateToPrefilledEditor.mock.calls[0][1].openInNewTab).toEqual(true); - }); - - test('Should render Inspect button', () => { - const { container } = render( - - - - ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - - expect(screen.getByText('Inspect')).toBeInTheDocument(); - expect(screen.getByText('Inspect')).not.toBeDisabled(); - }); - - test('Should render Inspect Modal after clicking the inspect button', () => { - const { baseElement, container } = render( - - - - ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - - expect(screen.getByText('Inspect')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Inspect')); - expect( - baseElement.querySelector('[data-test-subj="modal-inspect-euiModal"]') - ).toBeInTheDocument(); - }); - - test('Should render Add to new case button', () => { - const { container } = render( - - - - ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - - expect(screen.getByText('Add to new case')).toBeInTheDocument(); - expect(screen.getByText('Add to new case')).not.toBeDisabled(); - }); - - test('Should render Add to new case modal after clicking on Add to new case button', () => { - const { container } = render( - - - - ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - fireEvent.click(screen.getByText('Add to new case')); - - expect(mockGetCreateCaseFlyoutOpen).toBeCalled(); - }); - - test('Should render Add to existing case button', () => { - const { container } = render( - + test('Should render VisualizationActions button', async () => { + const { queryByTestId } = render( + ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - expect(screen.getByText('Add to existing case')).toBeInTheDocument(); - expect(screen.getByText('Add to existing case')).not.toBeDisabled(); + await waitFor(() => { + expect(queryByTestId(`stat-networkDnsHistogramQuery`)).toBeInTheDocument(); + }); }); - test('Should render Add to existing case modal after clicking on Add to existing case button', () => { - const { container } = render( - + test('renders context menu', async () => { + const { getByTestId } = render( + ); - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - fireEvent.click(screen.getByText('Add to existing case')); - - expect(mockGetAllCasesSelectorModalOpen).toBeCalled(); - }); - test('Should not render default actions when withDefaultActions = false', () => { - const testProps = { ...props, withDefaultActions: false }; - render( - - - - ); + await waitFor(() => { + expect(getByTestId(`stat-networkDnsHistogramQuery`)).toBeInTheDocument(); + }); - expect( - screen.queryByTestId(`[data-test-subj="stat-networkDnsHistogramQuery"]`) - ).not.toBeInTheDocument(); - expect(screen.queryByText('Inspect')).not.toBeInTheDocument(); - expect(screen.queryByText('Add to new case')).not.toBeInTheDocument(); - expect(screen.queryByText('Add to existing case')).not.toBeInTheDocument(); - expect(screen.queryByText('Open in Lens')).not.toBeInTheDocument(); - }); + fireEvent.click(getByTestId(`stat-networkDnsHistogramQuery`)); - test('Should render extra actions when extraAction is provided', () => { - const testProps = { - ...props, - extraActions: [ - { - getIconType: () => 'reset', - id: 'resetField', - execute: jest.fn(), - getDisplayName: () => 'Reset Field', - } as unknown as Action, - ], - }; - const { container } = render( - - - + expect(getByTestId('viz-actions-menu')).toBeInTheDocument(); + expect(mockContextMenu.mock.calls[0][0].panels[0].items[0].name).toEqual('Inspect'); + expect(mockContextMenu.mock.calls[0][0].panels[0].items[1].name).toEqual('Add to new case'); + expect(mockContextMenu.mock.calls[0][0].panels[0].items[2].name).toEqual( + 'Add to existing case' ); - - fireEvent.click(container.querySelector(`[data-test-subj="stat-networkDnsHistogramQuery"]`)!); - expect(screen.getByText('Reset Field')).toBeInTheDocument(); + expect(mockContextMenu.mock.calls[0][0].panels[1].items[0].name).toEqual('Added to library'); + expect(mockContextMenu.mock.calls[0][0].panels[1].items[1].name).toEqual('Open in Lens'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx index 2b996a48d2453..930e510ff07fa 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx @@ -123,14 +123,16 @@ const VisualizationActionsComponent: React.FC = ({ withActions, }); - const panels = useAsync(() => - buildContextMenuForActions({ - actions: contextMenuActions.map((action) => ({ - action, - context, - trigger: VISUALIZATION_CONTEXT_MENU_TRIGGER, - })), - }) + const panels = useAsync( + () => + buildContextMenuForActions({ + actions: contextMenuActions.map((action) => ({ + action, + context: {}, + trigger: VISUALIZATION_CONTEXT_MENU_TRIGGER, + })), + }), + [contextMenuActions] ); const button = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.test.tsx index 92f394006d8e9..c87e941b18c9d 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.test.tsx @@ -66,7 +66,7 @@ describe('LensEmbeddable', () => { queries: [ { id: 'testId', - inspect: { dsl: [], response: [] }, + inspect: { dsl: [], response: ['{"mockResponse": "mockResponse"}'] }, isInspected: false, loading: false, selectedInspectIndex: 0, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx index 273e4d89d1d7a..1582a0b382c75 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { NavigationProvider } from '@kbn/security-solution-navigation'; import { useKibana } from '../../lib/kibana/kibana_react'; import { mockAttributes } from './mocks'; -import { useActions } from './use_actions'; +import { DEFAULT_ACTIONS, useActions } from './use_actions'; import { coreMock } from '@kbn/core/public/mocks'; import { TestProviders } from '../../mock'; @@ -71,15 +71,15 @@ describe(`useActions`, () => { const { result } = renderHook( () => useActions({ - withActions: true, + withActions: DEFAULT_ACTIONS, attributes: mockAttributes, timeRange: { from: '2022-10-26T23:00:00.000Z', to: '2022-11-03T15:16:50.053Z', }, inspectActionProps: { - onInspectActionClicked: jest.fn(), - isDisabled: false, + handleInspectClick: jest.fn(), + isInspectButtonDisabled: false, }, }), { @@ -119,15 +119,15 @@ describe(`useActions`, () => { const { result } = renderHook( () => useActions({ - withActions: true, + withActions: DEFAULT_ACTIONS, attributes: mockAttributes, timeRange: { from: '2022-10-26T23:00:00.000Z', to: '2022-11-03T15:16:50.053Z', }, inspectActionProps: { - onInspectActionClicked: jest.fn(), - isDisabled: false, + handleInspectClick: jest.fn(), + isInspectButtonDisabled: false, }, extraActions: mockExtraAction, }), diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts index 5be61e8ff4cd4..8085097838307 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts @@ -7,6 +7,9 @@ import { useCallback, useMemo } from 'react'; import type { Action, Trigger } from '@kbn/ui-actions-plugin/public'; + +import { createAction } from '@kbn/ui-actions-plugin/public'; +import type { ActionDefinition } from '@kbn/ui-actions-plugin/public/actions'; import { useKibana } from '../../lib/kibana/kibana_react'; import { useAddToExistingCase } from './use_add_to_existing_case'; import { useAddToNewCase } from './use_add_to_new_case'; @@ -38,12 +41,53 @@ export const VISUALIZATION_CONTEXT_MENU_TRIGGER: Trigger = { id: 'VISUALIZATION_CONTEXT_MENU_TRIGGER', }; +const ACTION_DEFINITION: Record< + VisualizationContextMenuActions, + Omit +> = { + [VisualizationContextMenuActions.inspect]: { + id: VisualizationContextMenuActions.inspect, + getDisplayName: () => INSPECT, + getIconType: () => 'inspect', + type: 'actionButton', + order: 4, + }, + [VisualizationContextMenuActions.addToNewCase]: { + id: VisualizationContextMenuActions.addToNewCase, + getDisplayName: () => ADD_TO_NEW_CASE, + getIconType: () => 'casesApp', + type: 'actionButton', + order: 3, + }, + [VisualizationContextMenuActions.addToExistingCase]: { + id: VisualizationContextMenuActions.addToExistingCase, + getDisplayName: () => ADD_TO_EXISTING_CASE, + getIconType: () => 'casesApp', + type: 'actionButton', + order: 2, + }, + [VisualizationContextMenuActions.saveToLibrary]: { + id: VisualizationContextMenuActions.saveToLibrary, + getDisplayName: () => ADDED_TO_LIBRARY, + getIconType: () => 'save', + type: 'actionButton', + order: 1, + }, + [VisualizationContextMenuActions.openInLens]: { + id: VisualizationContextMenuActions.openInLens, + getDisplayName: () => OPEN_IN_LENS, + getIconType: () => 'visArea', + type: 'actionButton', + order: 0, + }, +}; + export const useActions = ({ attributes, - extraActions, + extraActions = [], inspectActionProps, timeRange, - withActions = [], + withActions = DEFAULT_ACTIONS, }: { attributes: LensAttributes | null; extraActions?: Action[]; @@ -88,73 +132,63 @@ export const useActions = ({ const { openSaveVisualizationFlyout, disableVisualizations } = useSaveToLibrary({ attributes }); - const contextMenuActionDefinitions = useMemo( - () => [ - ...(extraActions ?? []), - { - id: VisualizationContextMenuActions.inspect, - getDisplayName: () => INSPECT, - getIconType: () => 'inspect', - type: 'actionButton', - execute: async () => { - inspectActionProps.handleInspectClick(); - }, - disabled: inspectActionProps.isInspectButtonDisabled, - isCompatible: async () => withActions.includes(VisualizationContextMenuActions.inspect), - order: 4, - }, - { - id: VisualizationContextMenuActions.addToExistingCase, - getDisplayName: () => ADD_TO_EXISTING_CASE, - getIconType: () => 'casesApp', - type: 'actionButton', - execute: async () => { - onAddToExistingCaseClicked(); - }, - disabled: isAddToExistingCaseDisabled, - isCompatible: async () => - withActions.includes(VisualizationContextMenuActions.addToExistingCase), - order: 2, - }, - { - id: VisualizationContextMenuActions.addToNewCase, - getDisplayName: () => ADD_TO_NEW_CASE, - getIconType: () => 'casesApp', - type: 'actionButton', - execute: async () => { - onAddToNewCaseClicked(); - }, - disabled: isAddToNewCaseDisabled, - isCompatible: async () => - withActions.includes(VisualizationContextMenuActions.addToNewCase), - order: 3, - }, - { - id: VisualizationContextMenuActions.openInLens, - getDisplayName: () => OPEN_IN_LENS, - getIconType: () => 'visArea', - type: 'actionButton', - execute: async () => { - onOpenInLens(); - }, - isCompatible: async () => - canUseEditor() && withActions.includes(VisualizationContextMenuActions.openInLens), - order: 0, - }, - { - id: VisualizationContextMenuActions.saveToLibrary, - getDisplayName: () => ADDED_TO_LIBRARY, - getIconType: () => 'save', - type: 'actionButton', - execute: async () => { - openSaveVisualizationFlyout(); - }, - disabled: disableVisualizations, - isCompatible: async () => - withActions.includes(VisualizationContextMenuActions.saveToLibrary), - order: 1, - }, - ], + const allActions: Action[] = useMemo( + () => + [ + createAction({ + ...ACTION_DEFINITION[VisualizationContextMenuActions.inspect], + execute: async () => { + inspectActionProps.handleInspectClick(); + }, + disabled: inspectActionProps.isInspectButtonDisabled, + isCompatible: async () => withActions.includes(VisualizationContextMenuActions.inspect), + }), + createAction({ + ...ACTION_DEFINITION[VisualizationContextMenuActions.addToNewCase], + execute: async () => { + onAddToNewCaseClicked(); + }, + disabled: isAddToNewCaseDisabled, + isCompatible: async () => + withActions.includes(VisualizationContextMenuActions.addToNewCase), + }), + createAction({ + ...ACTION_DEFINITION[VisualizationContextMenuActions.addToExistingCase], + execute: async () => { + onAddToExistingCaseClicked(); + }, + disabled: isAddToExistingCaseDisabled, + isCompatible: async () => + withActions.includes(VisualizationContextMenuActions.addToExistingCase), + order: 2, + }), + createAction({ + ...ACTION_DEFINITION[VisualizationContextMenuActions.saveToLibrary], + execute: async () => { + openSaveVisualizationFlyout(); + }, + disabled: disableVisualizations, + isCompatible: async () => + withActions.includes(VisualizationContextMenuActions.saveToLibrary), + order: 1, + }), + createAction({ + ...ACTION_DEFINITION[VisualizationContextMenuActions.openInLens], + execute: async () => { + onOpenInLens(); + }, + isCompatible: async () => + canUseEditor() && withActions.includes(VisualizationContextMenuActions.openInLens), + order: 0, + }), + ...extraActions, + ].map((a, i, totalActions) => { + const order = Math.max(totalActions.length - (1 + i), 0); + return { + ...a, + order, + }; + }), [ canUseEditor, disableVisualizations, @@ -170,5 +204,5 @@ export const useActions = ({ ] ); - return contextMenuActionDefinitions; + return allActions; };