Skip to content

Commit

Permalink
[Security Solution] [Detection & Response] 131827 Update Detections R…
Browse files Browse the repository at this point in the history
…esponse view with pagination and opening numbers in timeline (elastic#131828)

* Fix alert colour pallete & alerts chart header size

* Add pagination and navigation to timeline capability

* fix translation name conflict

* Rename hook file to snake case to match elastic formatting

* Change name scheme oof navigateToTimeline to OpenInTimeline & remove styled components

Co-authored-by: Kristof-Pierre Cummings <kristofpierre.cummings@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored and j-bennet committed Jun 2, 2022
1 parent 38e2a91 commit d104450
Show file tree
Hide file tree
Showing 22 changed files with 617 additions and 338 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface UseActionCellDataProvider {
values: string[] | null | undefined;
}

const getDataProvider = (field: string, id: string, value: string): DataProvider => ({
export const getDataProvider = (field: string, id: string, value: string): DataProvider => ({
and: [],
enabled: true,
id: escapeDataProviderId(id),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,6 @@ const StyledLegendFlexItem = styled(EuiFlexItem)`
padding-top: 45px;
`;

// To Do remove this styled component once togglequery is updated: #131405
const StyledEuiPanel = styled(EuiPanel)`
height: fit-content;
`;

interface AlertsByStatusProps {
signalIndexName: string | null;
}
Expand Down Expand Up @@ -124,10 +119,7 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => {
return (
<>
<HoverVisibilityContainer show={true} targetClassNames={[INPECT_BUTTON_CLASS]}>
<StyledEuiPanel
hasBorder
data-test-subj={`${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}-panel`}
>
<EuiPanel hasBorder data-test-subj={`${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}-panel`}>
{loading && (
<EuiProgress
data-test-subj="initialLoadingPanelMatrixOverTime"
Expand All @@ -139,6 +131,7 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => {
<HeaderSection
id={DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}
title={ALERTS_TEXT}
titleSize="s"
subtitle={<LastUpdatedAt isUpdating={loading} updatedAt={updatedAt} />}
inspectMultiple
toggleStatus={toggleStatus}
Expand Down Expand Up @@ -212,7 +205,7 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => {
<EuiSpacer size="m" />
</>
)}
</StyledEuiPanel>
</EuiPanel>
</HoverVisibilityContainer>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import styled from 'styled-components';
import { FormattedNumber } from '@kbn/i18n-react';
import numeral from '@elastic/numeral';
import { BarChart } from '../../../../common/components/charts/barchart';
import { LastUpdatedAt } from '../util';
import { LastUpdatedAt } from '../utils';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { HeaderSection } from '../../../../common/components/header_section';
import {
Expand Down Expand Up @@ -112,10 +112,6 @@ const Wrapper = styled.div`
width: 100%;
`;

const StyledEuiPanel = styled(EuiPanel)`
height: 258px;
`;

const CasesByStatusComponent: React.FC = () => {
const { toggleStatus, setToggleStatus } = useQueryToggle(CASES_BY_STATUS_ID);
const { getAppUrl, navigateTo } = useNavigation();
Expand Down Expand Up @@ -155,7 +151,7 @@ const CasesByStatusComponent: React.FC = () => {
);

return (
<StyledEuiPanel hasBorder>
<EuiPanel hasBorder>
<HeaderSection
id={CASES_BY_STATUS_ID}
title={CASES_BY_STATUS_SECTION_TITLE}
Expand Down Expand Up @@ -194,7 +190,7 @@ const CasesByStatusComponent: React.FC = () => {
</StyledEuiFlexItem>
</EuiFlexGroup>
)}
</StyledEuiPanel>
</EuiPanel>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('CasesTable', () => {
mockUseCaseItemsReturn({ isLoading: false });
const { getByText } = renderComponent();

expect(getByText('Updated now')).toBeInTheDocument();
expect(getByText(/Updated/)).toBeInTheDocument();
});

it('should render the table columns', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { CaseDetailsLink } from '../../../../common/components/links';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { useNavigation, NavigateTo, GetAppUrl } from '../../../../common/lib/kibana';
import * as i18n from '../translations';
import { LastUpdatedAt } from '../util';
import { LastUpdatedAt } from '../utils';
import { StatusBadge } from './status_badge';
import { CaseItem, useCaseItems } from './use_case_items';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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 { useDispatch } from 'react-redux';

import { getDataProvider } from '../../../../common/components/event_details/table/use_action_cell_data_provider';
import { sourcererActions } from '../../../../common/store/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { DataProvider, TimelineId, TimelineType } from '../../../../../common/types/timeline';
import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline';
import { updateProviders } from '../../../../timelines/store/timeline/actions';

export const useNavigateToTimeline = () => {
const dispatch = useDispatch();

const clearTimeline = useCreateTimeline({
timelineId: TimelineId.active,
timelineType: TimelineType.default,
});

const navigateToTimeline = (dataProvider: DataProvider) => {
// Reset the current timeline
clearTimeline();
// Update the timeline's providers to match the current prevalence field query
dispatch(
updateProviders({
id: TimelineId.active,
providers: [dataProvider],
})
);
// Only show detection alerts
// (This is required so the timeline event count matches the prevalence count)
dispatch(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.timeline,
selectedDataViewId: 'security-solution-default',
selectedPatterns: ['.alerts-security.alerts-default'],
})
);
};

const openHostInTimeline = ({ hostName, severity }: { hostName: string; severity?: string }) => {
const dataProvider = getDataProvider('host.name', '', hostName);

if (severity) {
dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity));
}

navigateToTimeline(dataProvider);
};

const openUserInTimeline = ({ userName, severity }: { userName: string; severity?: string }) => {
const dataProvider = getDataProvider('user.name', '', userName);

if (severity) {
dataProvider.and.push(getDataProvider('kibana.alert.severity', '', severity));
}
navigateToTimeline(dataProvider);
};

const openRuleInTimeline = (ruleName: string) => {
const dataProvider = getDataProvider('kibana.alert.rule.name', '', ruleName);

navigateToTimeline(dataProvider);
};

return {
openHostInTimeline,
openRuleInTimeline,
openUserInTimeline,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React from 'react';

import { render } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';

import { TestProviders } from '../../../../common/mock';
import { parsedVulnerableHostsAlertsResult } from './mock_data';
Expand All @@ -30,6 +30,11 @@ const defaultUseHostAlertsItemsReturn: UseHostAlertsItemsReturn = {
items: [],
isLoading: false,
updatedAt: Date.now(),
pagination: {
currentPage: 0,
pageCount: 0,
setPage: () => null,
},
};
const mockUseHostAlertsItems = jest.fn(() => defaultUseHostAlertsItemsReturn);
const mockUseHostAlertsItemsReturn = (overrides: Partial<UseHostAlertsItemsReturn>) => {
Expand All @@ -47,34 +52,33 @@ const renderComponent = () =>
</TestProviders>
);

// FLAKY: https://github.com/elastic/kibana/issues/131611
describe.skip('HostAlertsTable', () => {
describe('HostAlertsTable', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render empty table', () => {
const { getByText, getByTestId } = renderComponent();
const { getByText, queryByTestId } = renderComponent();

expect(getByTestId('severityHostAlertsPanel')).toBeInTheDocument();
expect(queryByTestId('severityHostAlertsPanel')).toBeInTheDocument();
expect(queryByTestId('hostTablePaginator')).not.toBeInTheDocument();
expect(getByText('No alerts to display')).toBeInTheDocument();
expect(getByTestId('severityHostAlertsButton')).toBeInTheDocument();
});

it('should render a loading table', () => {
mockUseHostAlertsItemsReturn({ isLoading: true });
const { getByText, getByTestId } = renderComponent();
const { getByText, queryByTestId } = renderComponent();

expect(getByText('Updating...')).toBeInTheDocument();
expect(getByTestId('severityHostAlertsButton')).toBeInTheDocument();
expect(getByTestId('severityHostAlertsTable')).toHaveClass('euiBasicTable-loading');
expect(queryByTestId('severityHostAlertsTable')).toHaveClass('euiBasicTable-loading');
expect(queryByTestId('hostTablePaginator')).not.toBeInTheDocument();
});

it('should render the updated at subtitle', () => {
mockUseHostAlertsItemsReturn({ isLoading: false });
const { getByText } = renderComponent();

expect(getByText('Updated now')).toBeInTheDocument();
expect(getByText(/Updated/)).toBeInTheDocument();
});

it('should render the table columns', () => {
Expand All @@ -92,13 +96,32 @@ describe.skip('HostAlertsTable', () => {

it('should render the table items', () => {
mockUseHostAlertsItemsReturn({ items: [parsedVulnerableHostsAlertsResult[0]] });
const { getByTestId } = renderComponent();

expect(getByTestId('hostSeverityAlertsTable-hostName')).toHaveTextContent('Host-342m5gl1g2');
expect(getByTestId('hostSeverityAlertsTable-totalAlerts')).toHaveTextContent('100');
expect(getByTestId('hostSeverityAlertsTable-critical')).toHaveTextContent('5');
expect(getByTestId('hostSeverityAlertsTable-high')).toHaveTextContent('50');
expect(getByTestId('hostSeverityAlertsTable-medium')).toHaveTextContent('5');
expect(getByTestId('hostSeverityAlertsTable-low')).toHaveTextContent('40');
const { queryByTestId } = renderComponent();

expect(queryByTestId('hostSeverityAlertsTable-hostName')).toHaveTextContent('Host-342m5gl1g2');
expect(queryByTestId('hostSeverityAlertsTable-totalAlerts')).toHaveTextContent('100');
expect(queryByTestId('hostSeverityAlertsTable-critical')).toHaveTextContent('5');
expect(queryByTestId('hostSeverityAlertsTable-high')).toHaveTextContent('50');
expect(queryByTestId('hostSeverityAlertsTable-medium')).toHaveTextContent('5');
expect(queryByTestId('hostSeverityAlertsTable-low')).toHaveTextContent('40');
expect(queryByTestId('hostTablePaginator')).not.toBeInTheDocument();
});

it('should render the paginator if more than 4 results', () => {
const mockSetPage = jest.fn();

mockUseHostAlertsItemsReturn({
pagination: {
currentPage: 1,
pageCount: 3,
setPage: mockSetPage,
},
});
const { queryByTestId, getByText } = renderComponent();
const page3 = getByText('3');
expect(queryByTestId('hostTablePaginator')).toBeInTheDocument();

fireEvent.click(page3);
expect(mockSetPage).toHaveBeenCalledWith(2);
});
});
Loading

0 comments on commit d104450

Please sign in to comment.