[RAC] [Alerts] Add highlight to building block alerts (#107727)

* [rac] [Alerts] Add highlight to building block alerts

* Pull back 'additional filters' button to t-grid
This commit is contained in:
Pablo Machado 2021-08-10 12:31:05 +02:00 committed by GitHub
parent 78e2bd2788
commit eca7146a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 223 additions and 117 deletions

View file

@ -62,6 +62,7 @@ export interface OwnProps {
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode;
additionalFilters?: React.ReactNode;
}
type Props = OwnProps & PropsFromRedux;
@ -98,6 +99,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
showCheckboxes,
sort,
utilityBar,
additionalFilters,
// If truthy, the graph viewer (Resolver) is showing
graphEventId,
}) => {
@ -165,7 +167,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
setGlobalFullScreen,
start,
sort,
utilityBar,
additionalFilters,
graphEventId,
filterStatus: currentFilter,
leadingControlColumns,
@ -291,6 +293,7 @@ export const StatefulEventsViewer = connector(
prevProps.showCheckboxes === nextProps.showCheckboxes &&
prevProps.start === nextProps.start &&
prevProps.utilityBar === nextProps.utilityBar &&
prevProps.additionalFilters === nextProps.additionalFilters &&
prevProps.graphEventId === nextProps.graphEventId
)
);

View file

@ -50,14 +50,12 @@ const UtilityBarFlexGroup = styled(EuiFlexGroup)`
min-width: 175px;
`;
const BuildingBlockContainer = styled(EuiFlexItem)`
background: repeating-linear-gradient(
127deg,
rgba(245, 167, 0, 0.2),
rgba(245, 167, 0, 0.2) 1px,
rgba(245, 167, 0, 0.05) 2px,
rgba(245, 167, 0, 0.05) 10px
);
const AdditionalFiltersItem = styled(EuiFlexItem)`
padding: ${({ theme }) => theme.eui.paddingSizes.s};
`;
const BuildingBlockContainer = styled(AdditionalFiltersItem)`
background: ${({ theme }) => theme.eui.euiColorHighlight};
`;
const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
@ -146,39 +144,6 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
</UtilityBarFlexGroup>
);
const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => (
<UtilityBarFlexGroup direction="column" gutterSize="m">
<BuildingBlockContainer>
<EuiCheckbox
id="showBuildingBlockAlertsCheckbox"
aria-label="showBuildingBlockAlerts"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
closePopover();
onShowBuildingBlockAlertsChanged(e.target.checked);
}}
checked={showBuildingBlockAlerts}
color="text"
data-test-subj="showBuildingBlockAlertsCheckbox"
label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK}
/>
</BuildingBlockContainer>
<EuiFlexItem>
<EuiCheckbox
id="showOnlyThreatIndicatorAlertsCheckbox"
aria-label="showOnlyThreatIndicatorAlerts"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
closePopover();
onShowOnlyThreatIndicatorAlertsChanged(e.target.checked);
}}
checked={showOnlyThreatIndicatorAlerts}
color="text"
data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"
label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS}
/>
</EuiFlexItem>
</UtilityBarFlexGroup>
);
const handleSelectAllAlertsClick = useCallback(() => {
if (!showClearSelection) {
selectAll();
@ -233,16 +198,13 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
</>
)}
<UtilityBarSpacer />
<UtilityBarAction
dataTestSubj="additionalFilters"
disabled={areEventsLoading}
iconType="arrowDown"
iconSide="right"
ownFocus={true}
popoverContent={UtilityBarAdditionalFiltersContent}
>
{i18n.ADDITIONAL_FILTERS_ACTIONS}
</UtilityBarAction>
<AditionalFiltersAction
areEventsLoading={areEventsLoading}
onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged}
showBuildingBlockAlerts={showBuildingBlockAlerts}
onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsChanged}
showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts}
/>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
@ -260,3 +222,63 @@ export const AlertsUtilityBar = React.memo(
prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts &&
prevProps.showOnlyThreatIndicatorAlerts === nextProps.showOnlyThreatIndicatorAlerts
);
export const AditionalFiltersAction = ({
areEventsLoading,
onShowBuildingBlockAlertsChanged,
showBuildingBlockAlerts,
onShowOnlyThreatIndicatorAlertsChanged,
showOnlyThreatIndicatorAlerts,
}: {
areEventsLoading: boolean;
onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void;
showBuildingBlockAlerts: boolean;
onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void;
showOnlyThreatIndicatorAlerts: boolean;
}) => {
const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => (
<UtilityBarFlexGroup direction="column" gutterSize="none">
<BuildingBlockContainer>
<EuiCheckbox
id="showBuildingBlockAlertsCheckbox"
aria-label="showBuildingBlockAlerts"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
closePopover();
onShowBuildingBlockAlertsChanged(e.target.checked);
}}
checked={showBuildingBlockAlerts}
color="text"
data-test-subj="showBuildingBlockAlertsCheckbox"
label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK}
/>
</BuildingBlockContainer>
<AdditionalFiltersItem>
<EuiCheckbox
id="showOnlyThreatIndicatorAlertsCheckbox"
aria-label="showOnlyThreatIndicatorAlerts"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
closePopover();
onShowOnlyThreatIndicatorAlertsChanged(e.target.checked);
}}
checked={showOnlyThreatIndicatorAlerts}
color="text"
data-test-subj="showOnlyThreatIndicatorAlertsCheckbox"
label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS}
/>
</AdditionalFiltersItem>
</UtilityBarFlexGroup>
);
return (
<UtilityBarAction
dataTestSubj="additionalFilters"
disabled={areEventsLoading}
iconType="arrowDown"
iconSide="right"
ownFocus={true}
popoverContent={UtilityBarAdditionalFiltersContent}
>
{i18n.ADDITIONAL_FILTERS_ACTIONS}
</UtilityBarAction>
);
};

View file

@ -31,7 +31,7 @@ import {
alertsDefaultModelRuleRegistry,
buildAlertStatusFilterRuleRegistry,
} from './default_config';
import { AlertsUtilityBar } from './alerts_utility_bar';
import { AditionalFiltersAction, AlertsUtilityBar } from './alerts_utility_bar';
import * as i18nCommon from '../../../common/translations';
import * as i18n from './translations';
import {
@ -313,6 +313,16 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
]
);
const additionalFiltersComponent = (
<AditionalFiltersAction
areEventsLoading={loadingEventIds.length > 0}
onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged}
showBuildingBlockAlerts={showBuildingBlockAlerts}
onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsChanged}
showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts}
/>
);
const defaultFiltersMemo = useMemo(() => {
// TODO: Once we are past experimental phase this code should be removed
const alertStatusFilter = ruleRegistryEnabled
@ -382,6 +392,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
scopeId={SourcererScopeName.detections}
start={from}
utilityBar={utilityBarCallback}
additionalFilters={additionalFiltersComponent}
/>
);
};

View file

@ -14,8 +14,12 @@ import {
mapSortDirectionToDirection,
mapSortingColumns,
stringifyEvent,
addBuildingBlockStyle,
} from './helpers';
import { euiThemeVars } from '@kbn/ui-shared-deps/theme';
import { mockDnsEvent } from '../../../mock';
describe('helpers', () => {
describe('stringifyEvent', () => {
test('it omits __typename when it appears at arbitrary levels', () => {
@ -388,4 +392,32 @@ describe('helpers', () => {
).toBe(false);
});
});
describe('addBuildingBlockStyle', () => {
const THEME = { eui: euiThemeVars, darkMode: false };
test('it calls `setCellProps` with background color when event is a building block', () => {
const mockedSetCellProps = jest.fn();
const ecs = {
...mockDnsEvent,
...{ signal: { rule: { building_block_type: ['default'] } } },
};
addBuildingBlockStyle(ecs, THEME, mockedSetCellProps);
expect(mockedSetCellProps).toBeCalledWith({
style: {
backgroundColor: euiThemeVars.euiColorHighlight,
},
});
});
test('it call `setCellProps` reseting the background color when event is not a building block', () => {
const mockedSetCellProps = jest.fn();
addBuildingBlockStyle(mockDnsEvent, THEME, mockedSetCellProps);
expect(mockedSetCellProps).toBeCalledWith({ style: { backgroundColor: 'inherit' } });
});
});
});

View file

@ -7,6 +7,7 @@
import { isEmpty } from 'lodash/fp';
import { EuiDataGridCellValueElementProps } from '@elastic/eui';
import type { Ecs } from '../../../../common/ecs';
import type {
BrowserField,
@ -20,6 +21,8 @@ import type {
TimelineEventsType,
} from '../../../../common/types/timeline';
import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const omitTypenameAndEmpty = (k: string, v: any): any | undefined =>
k !== '__typename' && v != null ? v : undefined;
@ -185,3 +188,23 @@ export const allowSorting = ({
return isAllowlistedNonBrowserField || isAggregatable;
};
export const addBuildingBlockStyle = (
ecs: Ecs,
theme: EuiTheme,
setCellProps: EuiDataGridCellValueElementProps['setCellProps']
) => {
if (isEventBuildingBlockType(ecs)) {
setCellProps({
style: {
backgroundColor: `${theme.eui.euiColorHighlight}`,
},
});
} else {
// reset cell style
setCellProps({
style: {
backgroundColor: 'inherit',
},
});
}
};

View file

@ -24,28 +24,34 @@ import React, {
useEffect,
useMemo,
useState,
useContext,
} from 'react';
import { connect, ConnectedProps, useDispatch } from 'react-redux';
import { ThemeContext } from 'styled-components';
import {
TGridCellAction,
TimelineId,
TimelineTabs,
BulkActionsProp,
SortColumnTimeline,
} from '../../../../common/types/timeline';
import type {
CellValueElementProps,
ColumnHeaderOptions,
ControlColumnProps,
RowRenderer,
AlertStatus,
SortColumnTimeline,
TimelineId,
TimelineTabs,
} from '../../../../common/types/timeline';
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers';
import { getEventIdToDataMapping, mapSortDirectionToDirection, mapSortingColumns } from './helpers';
import {
addBuildingBlockStyle,
getEventIdToDataMapping,
mapSortDirectionToDirection,
mapSortingColumns,
} from './helpers';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
@ -58,6 +64,7 @@ import { RowAction } from './row_action';
import * as i18n from './translations';
import { AlertCount } from '../styles';
import { checkBoxControlColumn } from './control_columns';
import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common';
const StatefulAlertStatusBulkActions = lazy(
() => import('../toolbar/bulk_actions/alert_status_bulk_actions')
@ -121,6 +128,7 @@ const transformControlColumns = ({
onSelectPage,
browserFields,
sort,
theme,
}: {
actionColumnsWidth: number;
columnHeaders: ColumnHeaderOptions[];
@ -138,6 +146,7 @@ const transformControlColumns = ({
browserFields: BrowserFields;
onSelectPage: OnSelectAll;
sort: SortColumnTimeline[];
theme: EuiTheme;
}): EuiDataGridControlColumn[] =>
controlColumns.map(
({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({
@ -173,29 +182,33 @@ const transformControlColumns = ({
isExpanded,
rowIndex,
setCellProps,
}: EuiDataGridCellValueElementProps) => (
<RowAction
columnId={columnId ?? ''}
columnHeaders={columnHeaders}
controlColumn={controlColumns[i]}
data={data}
index={i}
isDetails={isDetails}
isExpanded={isExpanded}
isEventViewer={isEventViewer}
isExpandable={isExpandable}
loadingEventIds={loadingEventIds}
onRowSelected={onRowSelected}
onRuleChange={onRuleChange}
rowIndex={rowIndex}
selectedEventIds={selectedEventIds}
setCellProps={setCellProps}
showCheckboxes={showCheckboxes}
tabType={tabType}
timelineId={timelineId}
width={width ?? MIN_ACTION_COLUMN_WIDTH}
/>
),
}: EuiDataGridCellValueElementProps) => {
addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps);
return (
<RowAction
columnId={columnId ?? ''}
columnHeaders={columnHeaders}
controlColumn={controlColumns[i]}
data={data}
index={i}
isDetails={isDetails}
isExpanded={isExpanded}
isEventViewer={isEventViewer}
isExpandable={isExpandable}
loadingEventIds={loadingEventIds}
onRowSelected={onRowSelected}
onRuleChange={onRuleChange}
rowIndex={rowIndex}
selectedEventIds={selectedEventIds}
setCellProps={setCellProps}
showCheckboxes={showCheckboxes}
tabType={tabType}
timelineId={timelineId}
width={width ?? MIN_ACTION_COLUMN_WIDTH}
/>
);
},
width: width ?? actionColumnsWidth,
})
);
@ -252,6 +265,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]);
const theme: EuiTheme = useContext(ThemeContext);
const onRowSelected: OnRowSelected = useCallback(
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
setSelected({
@ -444,6 +458,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
sort,
browserFields,
onSelectPage,
theme,
})
);
}, [
@ -463,6 +478,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
browserFields,
onSelectPage,
sort,
theme,
]);
const columnsWithCellActions: EuiDataGridColumn[] = useMemo(
@ -483,34 +499,37 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
[browserFields, columnHeaders, data, defaultCellActions]
);
const renderTGridCellValue: (x: EuiDataGridCellValueElementProps) => React.ReactNode = ({
columnId,
rowIndex,
setCellProps,
}) => {
const rowData = rowIndex < data.length ? data[rowIndex].data : null;
const header = columnHeaders.find((h) => h.id === columnId);
const eventId = rowIndex < data.length ? data[rowIndex]._id : null;
const renderTGridCellValue: (
x: EuiDataGridCellValueElementProps
) => React.ReactNode = useCallback(
({ columnId, rowIndex, setCellProps }) => {
const rowData = rowIndex < data.length ? data[rowIndex].data : null;
const header = columnHeaders.find((h) => h.id === columnId);
const eventId = rowIndex < data.length ? data[rowIndex]._id : null;
if (rowData == null || header == null || eventId == null) {
return null;
}
addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps);
return renderCellValue({
columnId: header.id,
eventId,
data: rowData,
header,
isDraggable: false,
isExpandable: true,
isExpanded: false,
isDetails: false,
linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs),
rowIndex,
setCellProps,
timelineId: tabType != null ? `${id}-${tabType}` : id,
});
};
if (rowData == null || header == null || eventId == null) {
return null;
}
return renderCellValue({
columnId: header.id,
eventId,
data: rowData,
header,
isDraggable: false,
isExpandable: true,
isExpanded: false,
isDetails: false,
linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs),
rowIndex,
setCellProps,
timelineId: tabType != null ? `${id}-${tabType}` : id,
});
},
[columnHeaders, data, id, renderCellValue, tabType, theme]
);
return (
<EuiDataGrid

View file

@ -34,7 +34,6 @@ import {
DataPublicPluginStart,
} from '../../../../../../../src/plugins/data/public';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { Refetch } from '../../../store/t_grid/inputs';
import { defaultHeaders } from '../body/column_headers/default_headers';
import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers';
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
@ -42,7 +41,6 @@ import { useTimelineEvents } from '../../../container';
import { HeaderSection } from '../header_section';
import { StatefulBody } from '../body';
import { Footer, footerHeight } from '../footer';
import { LastUpdatedAt } from '../..';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
import * as i18n from '../translations';
import { ExitFullScreen } from '../../exit_full_screen';
@ -132,7 +130,7 @@ export interface TGridIntegratedProps {
setGlobalFullScreen: (fullscreen: boolean) => void;
start: string;
sort: Sort[];
utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode;
additionalFilters: React.ReactNode;
// If truthy, the graph viewer (Resolver) is showing
graphEventId: string | undefined;
leadingControlColumns?: ControlColumnProps[];
@ -167,7 +165,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
setGlobalFullScreen,
start,
sort,
utilityBar,
additionalFilters,
graphEventId,
leadingControlColumns,
trailingControlColumns,
@ -239,7 +237,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
const [
loading,
{ events, updatedAt, loadPage, pageInfo, refetch, totalCount = 0, inspect },
{ events, loadPage, pageInfo, refetch, totalCount = 0, inspect },
] = useTimelineEvents({
alertConsumers: SECURITY_ALERTS_CONSUMERS,
docValueFields,
@ -294,19 +292,17 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
height={
headerFilterGroup == null ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT
}
subtitle={utilityBar}
title={globalFullScreen ? titleWithExitFullScreen : justTitle}
>
{HeaderSectionContent}
</HeaderSection>
<EventsContainerLoading
data-timeline-id={id}
data-test-subj={`events-container-loading-${loading}`}
>
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<UpdatedFlexItem grow={false} show={!loading}>
<LastUpdatedAt updatedAt={updatedAt} />
{!resolverIsShowing(graphEventId) && additionalFilters}
</UpdatedFlexItem>
</EuiFlexGroup>