Skip to content

Commit

Permalink
[Security Solution][Timeline] extract and cleanup timeline bottom bar…
Browse files Browse the repository at this point in the history
… component (#173886)
  • Loading branch information
PhilippeOberti authored Jan 11, 2024
1 parent 9f2df50 commit 2b45409
Show file tree
Hide file tree
Showing 27 changed files with 278 additions and 411 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('Alert Flyout Automated Action Results', () => {
});
cy.contains(timelineRegex);
cy.getBySel('securitySolutionFlyoutNavigationCollapseDetailButton').click();
cy.getBySel('flyoutBottomBar').contains('Untitled timeline').click();
cy.getBySel('timeline-bottom-bar').contains('Untitled timeline').click();
cy.contains(filterRegex);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe(
cy.getBySel(RESULTS_TABLE_BUTTON).should('not.exist');
});
cy.contains('Cancel').click();
cy.getBySel('flyoutBottomBar').within(() => {
cy.getBySel('timeline-bottom-bar').within(() => {
cy.contains(TIMELINE_NAME).click();
});
cy.getBySel('draggableWrapperKeyboardHandler').contains('action_id: "');
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/osquery/cypress/e2e/all/timelines.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ describe.skip('ALL - Timelines', { tags: ['@ess'] }, () => {

it('should substitute osquery parameter on non-alert event take action', () => {
cy.visit('/app/security/timelines');
cy.getBySel('flyoutBottomBar').within(() => {
cy.getBySel('flyoutOverlay').click();
cy.getBySel('timeline-bottom-bar').within(() => {
cy.getBySel('timeline-bottom-bar-title-button').click();
});
cy.getBySel('timelineQueryInput').type('NOT host.name: "dev-fleet-server.8220"{enter}');
// Filter out alerts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ describe('when on the endpoint list page', () => {
});

const renderResult = render();
const timelineFlyout = renderResult.queryByTestId('flyoutOverlay');
const timelineFlyout = renderResult.queryByTestId('timeline-bottom-bar-title-button');
expect(timelineFlyout).toBeNull();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('Policy Details', () => {
});

it('should NOT display timeline', async () => {
expect(policyView.find('flyoutOverlay')).toHaveLength(0);
expect(policyView.find('timeline-bottom-bar-title-button')).toHaveLength(0);
});

it('should show loader followed by error message', async () => {
Expand Down Expand Up @@ -136,7 +136,7 @@ describe('Policy Details', () => {
it('should NOT display timeline', async () => {
policyView = render();
await asyncActions;
expect(policyView.find('flyoutOverlay')).toHaveLength(0);
expect(policyView.find('timeline-bottom-bar-title-button')).toHaveLength(0);
});

it('should display back to policy list button and policy title', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { TestProviders } from '../../../common/mock/test_providers';
import { timelineActions } from '../../store';
import { TimelineBottomBar } from '.';
import { TimelineId } from '../../../../common/types';

jest.mock('react-redux', () => {
const origin = jest.requireActual('react-redux');
return {
...origin,
useDispatch: jest.fn().mockReturnValue(jest.fn()),
};
});

describe('TimelineBottomBar', () => {
test('should render all components for bottom bar', () => {
const { getByTestId } = render(
<TestProviders>
<TimelineBottomBar show={false} timelineId={TimelineId.test} />
</TestProviders>
);

expect(getByTestId('timeline-bottom-bar')).toBeInTheDocument();
expect(getByTestId('timeline-event-count-badge')).toBeInTheDocument();
expect(getByTestId('timeline-save-status')).toBeInTheDocument();
expect(getByTestId('timeline-favorite-empty-star')).toBeInTheDocument();
});

test('should not render the event count badge if timeline is open', () => {
const { queryByTestId } = render(
<TestProviders>
<TimelineBottomBar show={true} timelineId={TimelineId.test} />
</TestProviders>
);

expect(queryByTestId('timeline-event-count-badge')).not.toBeInTheDocument();
});

test('should dispatch show action when clicking on the title', () => {
const spy = jest.spyOn(timelineActions, 'showTimeline');

const { getByTestId } = render(
<TestProviders>
<TimelineBottomBar show={true} timelineId={TimelineId.test} />
</TestProviders>
);

getByTestId('timeline-bottom-bar-title-button').click();

expect(spy).toHaveBeenCalledWith({
id: TimelineId.test,
show: true,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel } from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import type { State } from '../../../common/store';
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 { TimelineEventsCountBadge } from '../../../common/hooks/use_timeline_events_count';

const openTimelineButton = (title: string) =>
i18n.translate('xpack.securitySolution.timeline.bottomBar.toggleButtonAriaLabel', {
values: { title },
defaultMessage: 'Open timeline {title}',
});

interface TimelineBottomBarProps {
/**
* Id of the timeline to be displayed in the bottom bar and within the portal
*/
timelineId: string;
/**
* True if the timeline modal is open
*/
show: boolean;
}

/**
* This component renders the bottom bar for timeline displayed or most of the pages within Security Solution.
*/
export const TimelineBottomBar = React.memo<TimelineBottomBarProps>(({ show, timelineId }) => {
const dispatch = useDispatch();

const handleToggleOpen = useCallback(
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: true })),
[dispatch, timelineId]
);

const title = useSelector((state: State) => selectTitleByTimelineById(state, timelineId));

return (
<EuiPanel borderRadius="none" data-test-subj="timeline-bottom-bar">
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<AddTimelineButton timelineId={timelineId} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddToFavoritesButton timelineId={timelineId} compact />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
aria-label={openTimelineButton(title)}
onClick={handleToggleOpen}
data-test-subj="timeline-bottom-bar-title-button"
>
{title}
</EuiLink>
</EuiFlexItem>
{!show && ( // this is a hack because TimelineEventsCountBadge is using react-reverse-portal so the component which is used in multiple places cannot be visible in multiple places at the same time
<EuiFlexItem grow={false} data-test-subj="timeline-event-count-badge">
<TimelineEventsCountBadge />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<TimelineSaveStatus timelineId={timelineId} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
});

TimelineBottomBar.displayName = 'TimelineBottomBar';

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit 2b45409

Please sign in to comment.