mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SecuritySolution] Update Save Timeline button behaviour (#136724)
* Update Save Timeline button behaviour based on the user privileges Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
678d59e8ec
commit
a59ba34397
5 changed files with 155 additions and 54 deletions
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { getTimeline } from '../../objects/timeline';
|
||||
import { ROLES } from '../../../common/test';
|
||||
|
||||
import {
|
||||
LOCKED_ICON,
|
||||
|
@ -19,6 +20,8 @@ import {
|
|||
TIMELINE_PANEL,
|
||||
TIMELINE_TAB_CONTENT_EQL,
|
||||
TIMELINE_TAB_CONTENT_GRAPHS_NOTES,
|
||||
EDIT_TIMELINE_BTN,
|
||||
EDIT_TIMELINE_TOOLTIP,
|
||||
} from '../../screens/timeline';
|
||||
import { createTimelineTemplate } from '../../tasks/api_calls/timelines';
|
||||
|
||||
|
@ -48,6 +51,7 @@ describe('Create a timeline from a template', () => {
|
|||
createTimelineTemplate(getTimeline());
|
||||
visitWithoutDateRange(TIMELINE_TEMPLATES_URL);
|
||||
});
|
||||
|
||||
it('Should have the same query and open the timeline modal', () => {
|
||||
selectCustomTemplates();
|
||||
expandEventAction();
|
||||
|
@ -71,10 +75,30 @@ describe('Timelines', (): void => {
|
|||
after(() => {
|
||||
closeTimeline();
|
||||
});
|
||||
context('Privileges: CRUD', () => {
|
||||
it('toggle create timeline ', () => {
|
||||
createNewTimeline();
|
||||
addNameAndDescriptionToTimeline(getTimeline());
|
||||
cy.get(TIMELINE_PANEL).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('toggle create timeline ', () => {
|
||||
createNewTimeline();
|
||||
cy.get(TIMELINE_PANEL).should('be.visible');
|
||||
context('Privileges: READ', () => {
|
||||
before(() => {
|
||||
login(ROLES.reader);
|
||||
visit(OVERVIEW_URL, undefined, ROLES.reader);
|
||||
});
|
||||
it('should not be able to create/update timeline ', () => {
|
||||
createNewTimeline();
|
||||
cy.get(TIMELINE_PANEL).should('be.visible');
|
||||
cy.get(EDIT_TIMELINE_BTN).should('be.disabled');
|
||||
cy.get(EDIT_TIMELINE_BTN).first().trigger('mouseover', { force: true });
|
||||
cy.get(EDIT_TIMELINE_TOOLTIP).should('be.visible');
|
||||
cy.get(EDIT_TIMELINE_TOOLTIP).should(
|
||||
'have.text',
|
||||
'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -84,6 +108,8 @@ describe('Timelines', (): void => {
|
|||
});
|
||||
|
||||
before(() => {
|
||||
login();
|
||||
visit(OVERVIEW_URL);
|
||||
openTimelineUsingToggle();
|
||||
addNameAndDescriptionToTimeline(getTimeline());
|
||||
populateTimeline();
|
||||
|
|
|
@ -263,3 +263,7 @@ export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES =
|
|||
export const TIMESTAMP_HOVER_ACTION_OVERFLOW_BTN = '[data-test-subj="more-actions-@timestamp"]';
|
||||
|
||||
export const USER_KPI = '[data-test-subj="siem-timeline-user-kpi"]';
|
||||
|
||||
export const EDIT_TIMELINE_BTN = '[data-test-subj="save-timeline-button-icon"]';
|
||||
|
||||
export const EDIT_TIMELINE_TOOLTIP = '[data-test-subj="save-timeline-btn-tooltip"]';
|
||||
|
|
|
@ -312,9 +312,15 @@ export const waitForPage = (url: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const visit = (url: string, onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void) => {
|
||||
export const visit = (
|
||||
url: string,
|
||||
onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void,
|
||||
role?: ROLES
|
||||
) => {
|
||||
cy.visit(
|
||||
`${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`,
|
||||
`${
|
||||
role ? getUrlWithRoute(role, url) : url
|
||||
}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`,
|
||||
{
|
||||
onBeforeLoad(win) {
|
||||
if (onBeforeLoadCallback) {
|
||||
|
|
|
@ -6,9 +6,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
import type { SaveTimelineComponentProps } from './save_timeline_button';
|
||||
import { SaveTimelineButton } from './save_timeline_button';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
|
||||
const TEST_ID = {
|
||||
SAVE_TIMELINE_MODAL: 'save-timeline-modal',
|
||||
SAVE_TIMELINE_BTN: 'save-timeline-button-icon',
|
||||
SAVE_TIMELINE_TOOLTIP: 'save-timeline-tooltip',
|
||||
};
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const actual = jest.requireActual('react-redux');
|
||||
return {
|
||||
|
@ -16,59 +25,100 @@ jest.mock('react-redux', () => {
|
|||
useDispatch: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./title_and_description');
|
||||
|
||||
jest.mock('../../../../common/components/user_privileges');
|
||||
|
||||
const props = {
|
||||
initialFocus: 'title' as const,
|
||||
timelineId: 'timeline-1',
|
||||
toolTip: 'tooltip message',
|
||||
};
|
||||
|
||||
const TestSaveTimelineButton = (_props: SaveTimelineComponentProps) => (
|
||||
<TestProviders>
|
||||
<SaveTimelineButton {..._props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
jest.mock('raf', () => {
|
||||
return jest.fn().mockImplementation((cb) => cb());
|
||||
});
|
||||
|
||||
describe('SaveTimelineButton', () => {
|
||||
const props = {
|
||||
initialFocus: 'title' as const,
|
||||
timelineId: 'timeline-1',
|
||||
toolTip: 'tooltip message',
|
||||
};
|
||||
test('Show tooltip', () => {
|
||||
const component = mount(
|
||||
<TestProviders>
|
||||
<SaveTimelineButton {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('Hide tooltip', () => {
|
||||
const component = mount(
|
||||
<TestProviders>
|
||||
<SaveTimelineButton {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click');
|
||||
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(false);
|
||||
|
||||
// skipping this test because popover is not getting visible by RTL gestures.
|
||||
//
|
||||
// Raised a bug with eui team: https://github.com/elastic/eui/issues/6065
|
||||
xit('Show tooltip', async () => {
|
||||
render(<TestSaveTimelineButton {...props} />);
|
||||
const saveTimelineIcon = screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_BTN)[0];
|
||||
|
||||
fireEvent.mouseOver(saveTimelineIcon);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toBeVisible();
|
||||
});
|
||||
});
|
||||
test('should show a button with pencil icon', () => {
|
||||
const component = mount(
|
||||
<TestProviders>
|
||||
<SaveTimelineButton {...props} />
|
||||
</TestProviders>
|
||||
|
||||
it('should show a button with pencil icon', () => {
|
||||
render(<TestSaveTimelineButton {...props} />);
|
||||
|
||||
expect(screen.getByTestId(TEST_ID.SAVE_TIMELINE_BTN).firstChild).toHaveAttribute(
|
||||
'data-euiicon-type',
|
||||
'pencil'
|
||||
);
|
||||
expect(
|
||||
component.find('[data-test-subj="save-timeline-button-icon"]').first().prop('iconType')
|
||||
).toEqual('pencil');
|
||||
});
|
||||
test('should not show a modal when showOverlay equals false', () => {
|
||||
const component = mount(
|
||||
|
||||
it('should have edit timeline btn disabled with tooltip if user does not have write access', () => {
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
kibanaSecuritySolutionsPrivileges: { crud: false },
|
||||
});
|
||||
render(
|
||||
<TestProviders>
|
||||
<SaveTimelineButton {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(false);
|
||||
expect(screen.getByTestId(TEST_ID.SAVE_TIMELINE_BTN)).toBeDisabled();
|
||||
});
|
||||
test('should show a modal when showOverlay equals true', () => {
|
||||
const component = mount(
|
||||
<TestProviders>
|
||||
<SaveTimelineButton {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true);
|
||||
expect(component.find('[data-test-subj="save-timeline-modal-comp"]').exists()).toEqual(false);
|
||||
component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click');
|
||||
expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(false);
|
||||
expect(component.find('[data-test-subj="save-timeline-modal-comp"]').exists()).toEqual(true);
|
||||
|
||||
it('should not show modal if user does not have write access', async () => {
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
kibanaSecuritySolutionsPrivileges: { crud: false },
|
||||
});
|
||||
render(<TestSaveTimelineButton {...props} />);
|
||||
|
||||
expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).not.toBeInTheDocument();
|
||||
|
||||
const saveTimelineIcon = screen.getByTestId(TEST_ID.SAVE_TIMELINE_BTN);
|
||||
|
||||
fireEvent.click(saveTimelineIcon);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a modal when user has crud privileges', async () => {
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
kibanaSecuritySolutionsPrivileges: { crud: true },
|
||||
});
|
||||
render(<TestSaveTimelineButton {...props} />);
|
||||
expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_MODAL)).not.toBeInTheDocument();
|
||||
|
||||
const saveTimelineIcon = screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_BTN)[0];
|
||||
|
||||
fireEvent.click(saveTimelineIcon);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(TEST_ID.SAVE_TIMELINE_TOOLTIP)).not.toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId(TEST_ID.SAVE_TIMELINE_MODAL)[0]).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,13 +8,14 @@
|
|||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { timelineActions } from '../../../store/timeline';
|
||||
import { getTimelineSaveModalByIdSelector } from './selectors';
|
||||
import { TimelineTitleAndDescription } from './title_and_description';
|
||||
import { EDIT } from './translations';
|
||||
import * as timelineTranslations from './translations';
|
||||
|
||||
export interface SaveTimelineComponentProps {
|
||||
initialFocus: 'title' | 'description';
|
||||
|
@ -45,23 +46,37 @@ export const SaveTimelineButton = React.memo<SaveTimelineComponentProps>(
|
|||
setShowSaveTimelineOverlay(true);
|
||||
}, [setShowSaveTimelineOverlay]);
|
||||
|
||||
// Case: 1
|
||||
// check if user has crud privileges so that user can be allowed to edit the timeline
|
||||
// Case: 2
|
||||
// TODO: User may have Crud privileges but they may not have access to timeline index.
|
||||
// Do we need to check that?
|
||||
const {
|
||||
kibanaSecuritySolutionsPrivileges: { crud: hasKibanaCrud },
|
||||
} = useUserPrivileges();
|
||||
|
||||
const finalTooltipMsg = useMemo(
|
||||
() => (hasKibanaCrud ? toolTip : timelineTranslations.CALL_OUT_UNAUTHORIZED_MSG),
|
||||
[toolTip, hasKibanaCrud]
|
||||
);
|
||||
|
||||
const saveTimelineButtonIcon = useMemo(
|
||||
() => (
|
||||
<EuiButtonIcon
|
||||
aria-label={EDIT}
|
||||
aria-label={timelineTranslations.EDIT}
|
||||
isDisabled={!hasKibanaCrud}
|
||||
onClick={openSaveTimeline}
|
||||
iconType="pencil"
|
||||
data-test-subj="save-timeline-button-icon"
|
||||
/>
|
||||
),
|
||||
[openSaveTimeline]
|
||||
[openSaveTimeline, hasKibanaCrud]
|
||||
);
|
||||
|
||||
return (initialFocus === 'title' && show) || showSaveTimelineOverlay ? (
|
||||
<>
|
||||
{saveTimelineButtonIcon}
|
||||
<TimelineTitleAndDescription
|
||||
data-test-subj="save-timeline-modal-comp"
|
||||
closeSaveTimeline={closeSaveTimeline}
|
||||
initialFocus={initialFocus}
|
||||
timelineId={timelineId}
|
||||
|
@ -69,7 +84,7 @@ export const SaveTimelineButton = React.memo<SaveTimelineComponentProps>(
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<EuiToolTip content={toolTip ?? ''} data-test-subj="save-timeline-btn-tooltip">
|
||||
<EuiToolTip content={finalTooltipMsg ?? ''} data-test-subj="save-timeline-btn-tooltip">
|
||||
{saveTimelineButtonIcon}
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue