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 1582a0b382c75..45195d0982c5d 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 @@ -7,11 +7,8 @@ import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; -import { NavigationProvider } from '@kbn/security-solution-navigation'; -import { useKibana } from '../../lib/kibana/kibana_react'; import { mockAttributes } from './mocks'; import { DEFAULT_ACTIONS, useActions } from './use_actions'; -import { coreMock } from '@kbn/core/public/mocks'; import { TestProviders } from '../../mock'; jest.mock('./use_add_to_existing_case', () => { @@ -30,25 +27,20 @@ jest.mock('./use_add_to_new_case', () => { }), }; }); + +jest.mock('./use_redirect_to_dashboard_from_lens', () => ({ + useRedirectToDashboardFromLens: jest.fn().mockReturnValue({ + redirectTo: jest.fn(), + getEditOrCreateDashboardPath: jest.fn().mockReturnValue('mockDashboardPath'), + }), +})); + jest.mock('../../lib/kibana/kibana_react', () => { return { - useKibana: jest.fn(), - }; -}); - -const coreStart = coreMock.createStart(); -const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); -describe(`useActions`, () => { - const mockNavigateToPrefilledEditor = jest.fn(); - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ + useKibana: jest.fn().mockReturnValue({ services: { lens: { - navigateToPrefilledEditor: mockNavigateToPrefilledEditor, + navigateToPrefilledEditor: jest.fn(), canUseEditor: jest.fn().mockReturnValue(true), SaveModalComponent: jest .fn() @@ -59,33 +51,34 @@ describe(`useActions`, () => { addWarning: jest.fn(), }, }, + application: { capabilities: { visualize: { save: true } } }, }, - }); - }); + }), + }; +}); + +const props = { + withActions: DEFAULT_ACTIONS, + attributes: mockAttributes, + timeRange: { + from: '2022-10-26T23:00:00.000Z', + to: '2022-11-03T15:16:50.053Z', + }, + inspectActionProps: { + handleInspectClick: jest.fn(), + isInspectButtonDisabled: false, + }, +}; +describe(`useActions`, () => { beforeEach(() => { jest.clearAllMocks(); }); it('should render actions', () => { - const { result } = renderHook( - () => - useActions({ - withActions: DEFAULT_ACTIONS, - attributes: mockAttributes, - timeRange: { - from: '2022-10-26T23:00:00.000Z', - to: '2022-11-03T15:16:50.053Z', - }, - inspectActionProps: { - handleInspectClick: jest.fn(), - isInspectButtonDisabled: false, - }, - }), - { - wrapper, - } - ); + const { result } = renderHook(() => useActions(props), { + wrapper: TestProviders, + }); expect(result.current[0].id).toEqual('inspect'); expect(result.current[0].order).toEqual(4); expect(result.current[1].id).toEqual('addToNewCase'); @@ -119,20 +112,11 @@ describe(`useActions`, () => { const { result } = renderHook( () => useActions({ - withActions: DEFAULT_ACTIONS, - attributes: mockAttributes, - timeRange: { - from: '2022-10-26T23:00:00.000Z', - to: '2022-11-03T15:16:50.053Z', - }, - inspectActionProps: { - handleInspectClick: jest.fn(), - isInspectButtonDisabled: false, - }, + ...props, extraActions: mockExtraAction, }), { - wrapper, + wrapper: TestProviders, } ); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.test.tsx new file mode 100644 index 0000000000000..1350024564bb8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook, act } from '@testing-library/react-hooks'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { useSaveToLibrary } from './use_save_to_library'; +import { useKibana } from '../../lib/kibana'; +import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; + +jest.mock('../../lib/kibana', () => ({ + useKibana: jest.fn(), +})); + +jest.mock('./use_redirect_to_dashboard_from_lens', () => ({ + useRedirectToDashboardFromLens: jest.fn().mockReturnValue({ + redirectTo: jest.fn(), + getEditOrCreateDashboardPath: jest.fn().mockReturnValue('mockDashboardPath'), + }), +})); + +jest.mock('../link_to', () => ({ + useGetSecuritySolutionUrl: jest.fn(), +})); + +jest.mock('@kbn/react-kibana-mount', () => ({ + toMountPoint: jest.fn().mockReturnValue(jest.fn()), +})); + +const mockUseKibana = useKibana as jest.Mock; + +describe('useSaveToLibrary hook', () => { + const mockStartServices = { + application: { capabilities: { visualize: { save: true } } }, + lens: { SaveModalComponent: jest.fn() }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseKibana.mockReturnValue({ services: mockStartServices }); + }); + + it('should open the save visualization flyout when openSaveVisualizationFlyout is called', () => { + const { result } = renderHook(() => + useSaveToLibrary({ attributes: kpiHostMetricLensAttributes }) + ); + + act(() => { + result.current.openSaveVisualizationFlyout(); + }); + + expect(toMountPoint).toHaveBeenCalled(); + }); + + it('should disable visualizations if user cannot save', () => { + const noSaveCapabilities = { + ...mockStartServices, + application: { capabilities: { visualize: { save: false } } }, + }; + mockUseKibana.mockReturnValue({ services: noSaveCapabilities }); + + const { result } = renderHook(() => + useSaveToLibrary({ attributes: kpiHostMetricLensAttributes }) + ); + expect(result.current.disableVisualizations).toBe(true); + }); + + it('should disable visualizations if attributes are missing', () => { + mockUseKibana.mockReturnValue({ services: mockStartServices }); + const { result } = renderHook(() => useSaveToLibrary({ attributes: null })); + expect(result.current.disableVisualizations).toBe(true); + }); + + it('should enable visualizations if user can save and attributes are present', () => { + const { result } = renderHook(() => + useSaveToLibrary({ attributes: kpiHostMetricLensAttributes }) + ); + expect(result.current.disableVisualizations).toBe(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx index c7a44842fc513..ab3ff52eab674 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_save_to_library.tsx @@ -21,7 +21,8 @@ export const useSaveToLibrary = ({ attributes: LensAttributes | undefined | null; }) => { const startServices = useKibana().services; - const { SaveModalComponent, canUseEditor } = startServices.lens; + const canSaveVisualization = !!startServices.application.capabilities.visualize?.save; + const { SaveModalComponent } = startServices.lens; const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); const { redirectTo, getEditOrCreateDashboardPath } = useRedirectToDashboardFromLens({ getSecuritySolutionUrl, @@ -49,8 +50,8 @@ export const useSaveToLibrary = ({ }, [SaveModalComponent, attributes, getEditOrCreateDashboardPath, redirectTo, startServices]); const disableVisualizations = useMemo( - () => !canUseEditor() || attributes == null, - [attributes, canUseEditor] + () => !canSaveVisualization || attributes == null, + [attributes, canSaveVisualization] ); return { openSaveVisualizationFlyout, disableVisualizations };