Skip to content

Commit

Permalink
[Security Solution][Timeline] fix timeline favorite working for draft…
Browse files Browse the repository at this point in the history
… timeline (#175161)
  • Loading branch information
PhilippeOberti authored Jan 22, 2024
1 parent 99112c0 commit 0d06c17
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 271 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { mockTimelineModel, TestProviders } from '../../../common/mock';
import { AddToFavoritesButton } from '.';
import { TimelineStatus } from '../../../../common/api/timeline';

const mockGetState = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useSelector: (selector: any) =>
selector({
timeline: {
timelineById: {
'timeline-1': {
...mockGetState(),
},
},
},
}),
};
});

const renderAddFavoritesButton = () =>
render(
<TestProviders>
<AddToFavoritesButton timelineId="timeline-1" />
</TestProviders>
);

describe('AddToFavoritesButton', () => {
it('should render favorite button enabled and unchecked', () => {
mockGetState.mockReturnValue({
...mockTimelineModel,
status: TimelineStatus.active,
});

const { getByTestId, queryByTestId } = renderAddFavoritesButton();

const button = getByTestId('timeline-favorite-empty-star');

expect(button).toBeInTheDocument();
expect(button.firstChild).toHaveAttribute('data-euiicon-type', 'starEmpty');
expect(queryByTestId('timeline-favorite-filled-star')).not.toBeInTheDocument();
});

it('should render favorite button disabled for a draft timeline', () => {
mockGetState.mockReturnValue({
...mockTimelineModel,
status: TimelineStatus.draft,
});

const { getByTestId } = renderAddFavoritesButton();

expect(getByTestId('timeline-favorite-empty-star')).toHaveProperty('disabled');
});

it('should render favorite button disabled for an immutable timeline', () => {
mockGetState.mockReturnValue({
...mockTimelineModel,
status: TimelineStatus.immutable,
});

const { getByTestId } = renderAddFavoritesButton();

expect(getByTestId('timeline-favorite-empty-star')).toHaveProperty('disabled');
});

it('should render favorite button filled for a favorite timeline', () => {
mockGetState.mockReturnValue({
...mockTimelineModel,
isFavorite: true,
});

const { getByTestId, queryByTestId } = renderAddFavoritesButton();

expect(getByTestId('timeline-favorite-filled-star')).toBeInTheDocument();
expect(queryByTestId('timeline-favorite-empty-star')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { State } from '../../../common/store';
import { selectTimelineById } from '../../store/selectors';
import { timelineActions } from '../../store';
import { TimelineStatus } from '../../../../common/api/timeline';
import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../timeline/tour/step_config';

const ADD_TO_FAVORITES = i18n.translate(
'xpack.securitySolution.timeline.addToFavoriteButtonLabel',
{
defaultMessage: 'Add to favorites',
}
);

const REMOVE_FROM_FAVORITES = i18n.translate(
'xpack.securitySolution.timeline.removeFromFavoritesButtonLabel',
{
defaultMessage: 'Remove from favorites',
}
);

interface AddToFavoritesButtonProps {
/**
* Id of the timeline to be displayed in the bottom bar and within the modal
*/
timelineId: string;
}

/**
* This component renders the add to favorites button for timeline.
* It is used in the bottom bar as well as in the timeline modal's header.
*/
export const AddToFavoritesButton = React.memo<AddToFavoritesButtonProps>(({ timelineId }) => {
const dispatch = useDispatch();
const { isFavorite, status } = useSelector((state: State) =>
selectTimelineById(state, timelineId)
);

const isTimelineDraftOrImmutable = status !== TimelineStatus.active;
const label = isFavorite ? REMOVE_FROM_FAVORITES : ADD_TO_FAVORITES;

const handleClick = useCallback(
() => dispatch(timelineActions.updateIsFavorite({ id: timelineId, isFavorite: !isFavorite })),
[dispatch, timelineId, isFavorite]
);

return (
<EuiButtonIcon
id={TIMELINE_TOUR_CONFIG_ANCHORS.ADD_TO_FAVORITES}
iconType={isFavorite ? 'starFilled' : 'starEmpty'}
isSelected={isFavorite}
disabled={isTimelineDraftOrImmutable}
aria-label={label}
title={label}
data-test-subj={`timeline-favorite-${isFavorite ? 'filled' : 'empty'}-star`}
onClick={handleClick}
>
{label}
</EuiButtonIcon>
);
});

AddToFavoritesButton.displayName = 'AddToFavoritesButton';
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { selectTitleByTimelineById } from '../../store/selectors';
import { AddTimelineButton } from '../flyout/add_timeline_button';
import { timelineActions } from '../../store';
import { TimelineSaveStatus } from '../save_status';
import { AddToFavoritesButton } from '../timeline/properties/helpers';
import { AddToFavoritesButton } from '../add_to_favorites';
import { TimelineEventsCountBadge } from '../../../common/hooks/use_timeline_events_count';

const openTimelineButton = (title: string) =>
Expand Down Expand Up @@ -53,7 +53,7 @@ export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, tim
<AddTimelineButton timelineId={timelineId} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddToFavoritesButton timelineId={timelineId} compact />
<AddToFavoritesButton timelineId={timelineId} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { combineQueries } from '../../../../common/lib/kuery';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import * as i18n from './translations';
import { TimelineActionMenu } from '../action_menu';
import { AddToFavoritesButton } from '../../timeline/properties/helpers';
import { AddToFavoritesButton } from '../../add_to_favorites';
import { TimelineSaveStatus } from '../../save_status';
import { timelineDefaults } from '../../../store/defaults';
import { AddTimelineButton } from '../add_timeline_button';
Expand Down Expand Up @@ -127,7 +127,7 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<AddToFavoritesButton timelineId={timelineId} compact />
<AddToFavoritesButton timelineId={timelineId} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText grow={false} data-test-subj="timeline-title" css={whiteSpaceNoWrapCSS}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,3 @@ export const CLOSE_TIMELINE_OR_TEMPLATE = (isTimeline: boolean) =>
isTimeline,
},
});

export const TIMELINE_TOGGLE_BUTTON_ARIA_LABEL = ({
isOpen,
title,
}: {
isOpen: boolean;
title: string;
}) =>
i18n.translate('xpack.securitySolution.timeline.properties.timelineToggleButtonAriaLabel', {
values: { isOpen, title },
defaultMessage: '{isOpen, select, false {Open} true {Close} other {Toggle}} timeline {title}',
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,10 @@

import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';

import type { NewTimelineProps } from './helpers';
import { AddToFavoritesButton, NewTimeline } from './helpers';
import { NewTimeline } from './helpers';
import { useCreateTimelineButton } from './use_create_timeline';
import { kibanaObservable, TestProviders } from '../../../../common/mock/test_providers';
import { timelineActions } from '../../../store';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline';
import {
createSecuritySolutionStorageMock,
mockGlobalState,
SUB_PLUGINS_REDUCER,
} from '../../../../common/mock';
import { createStore } from '../../../../common/store';

jest.mock('./use_create_timeline');

Expand Down Expand Up @@ -94,144 +83,3 @@ describe('NewTimeline', () => {
});
});
});

describe('Favorite Button', () => {
describe('Non Elastic prebuilt templates', () => {
test('should render favorite button', () => {
const wrapper = mount(
<TestProviders>
<AddToFavoritesButton timelineId={TimelineId.test} />
</TestProviders>
);

expect(wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').exists()).toBeTruthy();
});

test('Favorite button should be enabled ', () => {
const wrapper = mount(
<TestProviders>
<AddToFavoritesButton timelineId={TimelineId.test} />
</TestProviders>
);

expect(
wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').first().prop('disabled')
).toEqual(false);
});

test('Should update isFavorite after clicking on favorite button', async () => {
const spy = jest.spyOn(timelineActions, 'updateIsFavorite');
const wrapper = mount(
<TestProviders>
<AddToFavoritesButton timelineId={TimelineId.test} />
</TestProviders>
);

waitFor(() => {
wrapper.simulate('click');
expect(spy).toHaveBeenCalled();
});
});

test('should disable favorite button with filled star', () => {
const { storage } = createSecuritySolutionStorageMock();

const store = createStore(
{
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
isFavorite: true,
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const wrapper = mount(
<TestProviders store={store}>
<AddToFavoritesButton timelineId={TimelineId.test} />
</TestProviders>
);

expect(
wrapper.find('[data-test-subj="timeline-favorite-filled-star"]').exists()
).toBeTruthy();
});
});

describe('Elast prebuilt templates', () => {
test('should disable favorite button', () => {
const { storage } = createSecuritySolutionStorageMock();

const store = createStore(
{
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
status: TimelineStatus.immutable,
timelineType: TimelineType.template,
templateTimelineId: 'mock-template-timeline-id',
templateTimelineVersion: 1,
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const wrapper = mount(
<TestProviders store={store}>
<AddToFavoritesButton timelineId={TimelineId.test} />
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').first().prop('disabled')
).toEqual(true);
});
});

describe('Custom templates', () => {
test('should enable favorite button', () => {
const { storage } = createSecuritySolutionStorageMock();

const store = createStore(
{
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
status: TimelineStatus.active,
timelineType: TimelineType.template,
templateTimelineId: 'mock-template-timeline-id',
templateTimelineVersion: 1,
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const wrapper = mount(
<TestProviders store={store}>
<AddToFavoritesButton timelineId="test" />
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').first().prop('disabled')
).toEqual(false);
});
});
});
Loading

0 comments on commit 0d06c17

Please sign in to comment.