[Security Solution] Rule Preview Table Follow-up (#128981)

This commit is contained in:
Davis Plumlee 2022-04-11 15:34:58 -04:00 committed by GitHub
parent 4ff0a6e0af
commit 268470a440
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 427 additions and 179 deletions

View file

@ -6,7 +6,7 @@
*/
export enum RULE_PREVIEW_INVOCATION_COUNT {
HOUR = 20,
HOUR = 12,
DAY = 24,
WEEK = 168,
MONTH = 30,

View file

@ -326,6 +326,7 @@ export enum TimelineId {
casePage = 'timeline-case',
test = 'test', // Reserved for testing purposes
alternateTest = 'alternateTest',
rulePreview = 'rule-preview',
}
export const TimelineIdLiteralRt = runtimeTypes.union([
@ -339,6 +340,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([
runtimeTypes.literal(TimelineId.networkPageExternalAlerts),
runtimeTypes.literal(TimelineId.active),
runtimeTypes.literal(TimelineId.test),
runtimeTypes.literal(TimelineId.rulePreview),
]);
export type TimelineIdLiteral = runtimeTypes.TypeOf<typeof TimelineIdLiteralRt>;

View file

@ -327,9 +327,9 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => {
cy.get(PREVIEW_HISTOGRAM)
.invoke('text')
.then((text) => {
if (text !== 'Hits') {
if (text !== 'Rule Preview') {
cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true });
cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Hits');
cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Rule Preview');
}
});
cy.get(TOAST_ERROR).should('not.exist');

View file

@ -82,6 +82,15 @@ describe('AlertSummaryView', () => {
expect(queryAllByTestId('hover-actions-filter-for').length).toEqual(0);
});
test('it does NOT render the action cell when readOnly is passed', () => {
const { queryAllByTestId } = render(
<TestProviders>
<AlertSummaryView {...{ ...props, isReadOnly: true }} />
</TestProviders>
);
expect(queryAllByTestId('hover-actions-filter-for').length).toEqual(0);
});
test("render no investigation guide if it doesn't exist", async () => {
(useRuleWithFallback as jest.Mock).mockReturnValue({
rule: {

View file

@ -136,5 +136,18 @@ describe('getColumns', () => {
).toEqual('hover-actions-copy-button');
});
});
describe('does not render hover actions when readOnly prop is passed', () => {
test('it renders a filter for (+) button', () => {
actionsColumn = getColumns({ ...defaultProps, isReadOnly: true })[0] as Column;
const wrapper = mount(
<TestProviders>{actionsColumn.render(testValue, testData)}</TestProviders>
) as ReactWrapper;
expect(wrapper.find('[data-test-subj="hover-actions-filter-for"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="hover-actions-filter-out"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="more-actions-agent.id"]').exists()).toBeFalsy();
});
});
});
});

View file

@ -140,5 +140,20 @@ describe('EventDetails', () => {
alertsWrapper.find('[data-test-subj="threatIntelTab"]').first().simulate('click');
expect(alertsWrapper.find('[data-test-subj="no-enrichments-found"]').exists()).toEqual(true);
});
it('does not render if readOnly prop is passed', async () => {
const newProps = { ...defaultProps, isReadOnly: true };
wrapper = mount(
<TestProviders>
<EventDetails {...newProps} />
</TestProviders>
) as ReactWrapper;
alertsWrapper = mount(
<TestProviders>
<EventDetails {...{ ...alertsProps, ...newProps }} />
</TestProviders>
) as ReactWrapper;
await waitFor(() => wrapper.update());
expect(alertsWrapper.find('[data-test-subj="threatIntelTab"]').exists()).toBeFalsy();
});
});
});

View file

@ -132,6 +132,25 @@ describe('EventFieldsBrowser', () => {
expect(wrapper.find('[data-test-subj="more-actions-@timestamp"]').exists()).toBeTruthy();
});
test('it does not render hover actions when readOnly prop is passed', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={eventId}
timelineId="test"
timelineTabType={TimelineTabs.query}
isReadOnly
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="hover-actions-filter-for"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="hover-actions-filter-out"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="more-actions-@timestamp"]').exists()).toBeFalsy();
});
test('it renders a column toggle button', () => {
const wrapper = mount(
<TestProviders>

View file

@ -29,6 +29,20 @@ describe('Event Details Overview Cards', () => {
getByText('Rule');
});
it('renders only readOnly cards', () => {
const { getByText, queryByText } = render(
<TestProviders>
<Overview {...propsWithReadOnly} />
</TestProviders>
);
getByText('Severity');
getByText('Risk Score');
expect(queryByText('Status')).not.toBeInTheDocument();
expect(queryByText('Rule')).not.toBeInTheDocument();
});
it('renders all cards it has data for', () => {
const { getByText, queryByText } = render(
<TestProviders>
@ -194,3 +208,8 @@ const propsWithoutSeverity = {
browserFields: { kibana: { fields: fieldsWithoutSeverity } },
data: dataWithoutSeverity,
};
const propsWithReadOnly = {
...props,
isReadOnly: true,
};

View file

@ -8,7 +8,7 @@
import { useState, useEffect } from 'react';
import { useRouteSpy } from '../route/use_route_spy';
const hideTimelineForRoutes = [`/cases/configure`, '/administration'];
const hideTimelineForRoutes = [`/cases/configure`, '/administration', 'rules/create'];
export const useShowTimeline = () => {
const [{ pageName, pathName }] = useRouteSpy();

View file

@ -16,7 +16,10 @@ import { RowRendererId } from '../../../../common/types/timeline';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { columns } from '../../configurations/security_solution_detections/columns';
import {
columns,
rulePreviewColumns,
} from '../../configurations/security_solution_detections/columns';
export const buildAlertStatusFilter = (status: Status): Filter[] => {
const combinedQuery =
@ -156,6 +159,19 @@ export const alertsDefaultModel: SubsetTimelineModel = {
excludedRowRendererIds: Object.values(RowRendererId),
};
export const alertsPreviewDefaultModel: SubsetTimelineModel = {
...alertsDefaultModel,
columns: rulePreviewColumns,
defaultColumns: rulePreviewColumns,
sort: [
{
columnId: 'kibana.alert.original_time',
columnType: 'number',
sortDirection: 'desc',
},
],
};
export const requiredFieldsForActions = [
'@timestamp',
'kibana.alert.workflow_status',

View file

@ -234,7 +234,7 @@ export const getIsRulePreviewDisabled = ({
if (ruleType === 'machine_learning') {
return machineLearningJobId.length === 0;
}
if (ruleType === 'eql') {
if (ruleType === 'eql' || ruleType === 'query' || ruleType === 'threshold') {
return queryBar.query.query.length === 0;
}
return false;

View file

@ -29,6 +29,13 @@ import { LoadingHistogram } from './loading_histogram';
import { FieldValueThreshold } from '../threshold_input';
import { isJobStarted } from '../../../../../common/machine_learning/helpers';
const HelpTextComponent = (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>{i18n.QUERY_PREVIEW_HELP_TEXT}</EuiFlexItem>
<EuiFlexItem>{i18n.QUERY_PREVIEW_DISCLAIMER}</EuiFlexItem>
</EuiFlexGroup>
);
export interface RulePreviewProps {
index: string[];
isDisabled: boolean;
@ -116,7 +123,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
<>
<EuiFormRow
label={i18n.QUERY_PREVIEW_LABEL}
helpText={i18n.QUERY_PREVIEW_HELP_TEXT}
helpText={HelpTextComponent}
error={undefined}
isInvalid={false}
data-test-subj="rule-preview"
@ -156,7 +163,6 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
previewId={previewId}
addNoiseWarning={addNoiseWarning}
spaceId={spaceId}
threshold={threshold}
index={index}
/>
)}

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { render } from '@testing-library/react';
import * as i18n from '../rule_preview/translations';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
@ -14,10 +14,12 @@ import { TestProviders } from '../../../../common/mock';
import { usePreviewHistogram } from './use_preview_histogram';
import { PreviewHistogram } from './preview_histogram';
import { ALL_VALUES_ZEROS_TITLE } from '../../../../common/components/charts/translation';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/containers/use_global_time');
jest.mock('./use_preview_histogram');
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/components/url_state/normalize_time_range.ts');
describe('PreviewHistogram', () => {
const mockSetQuery = jest.fn();
@ -35,6 +37,37 @@ describe('PreviewHistogram', () => {
jest.clearAllMocks();
});
describe('when there is no data', () => {
(usePreviewHistogram as jest.Mock).mockReturnValue([
false,
{
inspect: { dsl: [], response: [] },
totalCount: 1,
refetch: jest.fn(),
data: [],
buckets: [],
},
]);
test('it renders an empty histogram and table', () => {
const wrapper = render(
<TestProviders>
<PreviewHistogram
addNoiseWarning={jest.fn()}
timeFrame="M"
previewId={'test-preview-id'}
spaceId={'default'}
ruleType={'query'}
index={['']}
/>
</TestProviders>
);
expect(wrapper.findByText('hello grid')).toBeTruthy();
expect(wrapper.findByText(ALL_VALUES_ZEROS_TITLE)).toBeTruthy();
});
});
test('it renders loader when isLoading is true', () => {
(usePreviewHistogram as jest.Mock).mockReturnValue([
true,
@ -47,7 +80,7 @@ describe('PreviewHistogram', () => {
},
]);
const wrapper = mount(
const wrapper = render(
<TestProviders>
<PreviewHistogram
addNoiseWarning={jest.fn()}
@ -60,72 +93,7 @@ describe('PreviewHistogram', () => {
</TestProviders>
);
expect(wrapper.find('[data-test-subj="preview-histogram-loading"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="header-section-subtitle"]').text()).toEqual(
i18n.QUERY_PREVIEW_SUBTITLE_LOADING
);
});
test('it configures data and subtitle', () => {
(usePreviewHistogram as jest.Mock).mockReturnValue([
false,
{
inspect: { dsl: [], response: [] },
totalCount: 9154,
refetch: jest.fn(),
data: [
{ x: 1602247050000, y: 2314, g: 'All others' },
{ x: 1602247162500, y: 3471, g: 'All others' },
{ x: 1602247275000, y: 3369, g: 'All others' },
],
buckets: [],
},
]);
const wrapper = mount(
<TestProviders>
<PreviewHistogram
addNoiseWarning={jest.fn()}
timeFrame="M"
previewId={'test-preview-id'}
spaceId={'default'}
ruleType={'query'}
index={['']}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="preview-histogram-loading"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="header-section-subtitle"]').text()).toEqual(
i18n.QUERY_PREVIEW_TITLE(9154)
);
expect(
(
wrapper.find('[data-test-subj="preview-histogram-bar-chart"]').props() as {
barChart: unknown;
}
).barChart
).toEqual([
{
key: 'hits',
value: [
{
g: 'All others',
x: 1602247050000,
y: 2314,
},
{
g: 'All others',
x: 1602247162500,
y: 3471,
},
{
g: 'All others',
x: 1602247275000,
y: 3369,
},
],
},
]);
expect(wrapper.findByTestId('preview-histogram-loading')).toBeTruthy();
expect(wrapper.findByText(i18n.QUERY_PREVIEW_SUBTITLE_LOADING)).toBeTruthy();
});
});

View file

@ -11,19 +11,20 @@ import { Unit } from '@kbn/datemath';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui';
import styled from 'styled-components';
import { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import { useDispatch, useSelector } from 'react-redux';
import { eventsViewerSelector } from '../../../../common/components/events_viewer/selectors';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useKibana } from '../../../../common/lib/kibana';
import * as i18n from './translations';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { getHistogramConfig, getThresholdHistogramConfig, isNoisy } from './helpers';
import { getHistogramConfig, isNoisy } from './helpers';
import { ChartSeriesConfigs, ChartSeriesData } from '../../../../common/components/charts/common';
import { Panel } from '../../../../common/components/panel';
import { HeaderSection } from '../../../../common/components/header_section';
import { BarChart } from '../../../../common/components/charts/barchart';
import { usePreviewHistogram } from './use_preview_histogram';
import { formatDate } from '../../../../common/components/super_date_picker';
import { FieldValueThreshold } from '../threshold_input';
import { alertsDefaultModel } from '../../alerts_table/default_config';
import { alertsPreviewDefaultModel } from '../../alerts_table/default_config';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers';
import { TimelineId } from '../../../../../common/types';
@ -35,6 +36,8 @@ import { PreviewRenderCellValue } from './preview_table_cell_renderer';
import { getPreviewTableControlColumn } from './preview_table_control_columns';
import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen';
import { InspectButtonContainer } from '../../../../common/components/inspect';
import { timelineActions } from '../../../../timelines/store/timeline';
import { State } from '../../../../common/store';
const LoadingChart = styled(EuiLoadingChart)`
display: block;
@ -55,7 +58,6 @@ interface PreviewHistogramProps {
previewId: string;
addNoiseWarning: () => void;
spaceId: string;
threshold?: FieldValueThreshold;
ruleType: Type;
index: string[];
}
@ -67,10 +69,10 @@ export const PreviewHistogram = ({
previewId,
addNoiseWarning,
spaceId,
threshold,
ruleType,
index,
}: PreviewHistogramProps) => {
const dispatch = useDispatch();
const { setQuery, isInitializing } = useGlobalTime();
const { timelines: timelinesUi, cases } = useKibana().services;
const from = useMemo(() => `now-1${timeFrame}`, [timeFrame]);
@ -78,34 +80,36 @@ export const PreviewHistogram = ({
const startDate = useMemo(() => formatDate(from), [from]);
const endDate = useMemo(() => formatDate(to), [to]);
const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]);
const isThresholdRule = useMemo(() => ruleType === 'threshold', [ruleType]);
const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]);
const [isLoading, { data, inspect, totalCount, refetch, buckets }] = usePreviewHistogram({
const [isLoading, { data, inspect, totalCount, refetch }] = usePreviewHistogram({
previewId,
startDate,
endDate,
spaceId,
threshold: isThresholdRule ? threshold : undefined,
index,
ruleType,
});
const {
columns,
dataProviders,
deletedEventIds,
kqlMode,
itemsPerPage,
itemsPerPageOptions,
graphEventId,
sort,
} = alertsDefaultModel;
timeline: {
columns,
dataProviders,
defaultColumns,
deletedEventIds,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
sort,
} = alertsPreviewDefaultModel,
} = useSelector((state: State) => eventsViewerSelector(state, TimelineId.rulePreview));
const {
browserFields,
docValueFields,
indexPattern,
runtimeMappings,
dataViewId: selectedDataViewId,
loading: isLoadingIndexPattern,
} = useSourcererDataView(SourcererScopeName.detections);
@ -129,42 +133,28 @@ export const PreviewHistogram = ({
}
}, [setQuery, inspect, isLoading, isInitializing, refetch, previewId]);
useEffect(() => {
dispatch(
timelineActions.createTimeline({
columns,
dataViewId: selectedDataViewId,
defaultColumns,
id: TimelineId.rulePreview,
indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`],
itemsPerPage,
sort,
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const barConfig = useMemo(
(): ChartSeriesConfigs => getHistogramConfig(endDate, startDate, !isEqlRule),
[endDate, startDate, isEqlRule]
);
const thresholdBarConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(), []);
const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]);
const { thresholdChartData, thresholdTotalCount } = useMemo((): {
thresholdChartData: ChartSeriesData[];
thresholdTotalCount: number;
} => {
const total = buckets.length;
const dataBuckets = buckets.map<{ x: string; y: number; g: string }>(
({ key, doc_count: docCount }) => ({
x: key,
y: docCount,
g: key,
})
);
return {
thresholdChartData: [{ key: 'hits', value: dataBuckets }],
thresholdTotalCount: total,
};
}, [buckets]);
const subtitle = useMemo(
(): string =>
isLoading
? i18n.QUERY_PREVIEW_SUBTITLE_LOADING
: isThresholdRule
? i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(thresholdTotalCount)
: i18n.QUERY_PREVIEW_TITLE(totalCount),
[isLoading, totalCount, thresholdTotalCount, isThresholdRule]
);
const CasesContext = cases.ui.getCasesContext();
return (
@ -176,7 +166,6 @@ export const PreviewHistogram = ({
id={`${ID}-${previewId}`}
title={i18n.QUERY_GRAPH_HITS_TITLE}
titleSize="xs"
subtitle={subtitle}
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
@ -184,8 +173,8 @@ export const PreviewHistogram = ({
<LoadingChart size="l" data-test-subj="preview-histogram-loading" />
) : (
<BarChart
configs={isThresholdRule ? thresholdBarConfig : barConfig}
barChart={isThresholdRule ? thresholdChartData : chartData}
configs={barConfig}
barChart={chartData}
data-test-subj="preview-histogram-bar-chart"
/>
)}
@ -194,7 +183,11 @@ export const PreviewHistogram = ({
<>
<EuiSpacer />
<EuiText size="s" color="subdued">
<p>{i18n.QUERY_PREVIEW_DISCLAIMER_MAX_SIGNALS}</p>
<p>
{isMlRule
? i18n.ML_PREVIEW_HISTOGRAM_DISCLAIMER
: i18n.PREVIEW_HISTOGRAM_DISCLAIMER}
</p>
</EuiText>
</>
</EuiFlexItem>
@ -213,13 +206,12 @@ export const PreviewHistogram = ({
deletedEventIds,
disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS,
docValueFields,
end: to,
entityType: 'alerts',
end: endDate,
entityType: 'events',
filters: [],
globalFullScreen,
graphEventId,
hasAlertsCrud: false,
id: TimelineId.detectionsPage,
id: TimelineId.rulePreview,
indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`],
indexPattern,
isLive: false,
@ -233,7 +225,7 @@ export const PreviewHistogram = ({
runtimeMappings,
setQuery: () => {},
sort,
start: from,
start: startDate,
tGridEventRenderedViewEnabled,
type: 'embedded',
leadingControlColumns: getPreviewTableControlColumn(1.5),
@ -242,11 +234,11 @@ export const PreviewHistogram = ({
</FullScreenContainer>
<DetailsPanel
browserFields={browserFields}
entityType={'alerts'}
entityType={'events'}
docValueFields={docValueFields}
isFlyoutView
runtimeMappings={runtimeMappings}
timelineId={TimelineId.detectionsPage}
timelineId={TimelineId.rulePreview}
isReadOnly
/>
</CasesContext>

View file

@ -0,0 +1,167 @@
/*
* 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 { mount } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
import { mockBrowserFields } from '../../../../common/containers/source/mock';
import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper';
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock';
import { PreviewTableCellRenderer } from './preview_table_cell_renderer';
import { getColumnRenderer } from '../../../../timelines/components/timeline/body/renderers/get_column_renderer';
import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper';
import { BrowserFields } from '../../../../../../timelines/common/search_strategy';
import { Ecs } from '../../../../../common/ecs';
import { columnRenderers } from '../../../../timelines/components/timeline/body/renderers';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../timelines/components/timeline/body/renderers/get_column_renderer');
const getColumnRendererMock = getColumnRenderer as jest.Mock;
const mockImplementation = {
renderColumn: jest.fn(),
};
describe('PreviewTableCellRenderer', () => {
const columnId = '@timestamp';
const eventId = '_id-123';
const isExpandable = true;
const isExpanded = true;
const linkValues = ['foo', 'bar', '@baz'];
const rowIndex = 3;
const colIndex = 0;
const setCellProps = jest.fn();
const timelineId = 'test';
const ecsData = {} as Ecs;
const browserFields = {} as BrowserFields;
beforeEach(() => {
jest.clearAllMocks();
getColumnRendererMock.mockImplementation(() => mockImplementation);
});
test('it invokes `getColumnRenderer` with the expected arguments', () => {
const data = cloneDeep(mockTimelineData[0].data);
const header = cloneDeep(defaultHeaders[0]);
const isDetails = true;
mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper droppableId="testing">
<PreviewTableCellRenderer
browserFields={browserFields}
columnId={columnId}
data={data}
ecsData={ecsData}
eventId={eventId}
header={header}
isDetails={isDetails}
isDraggable={true}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
setCellProps={setCellProps}
timelineId={timelineId}
/>
</DroppableWrapper>
</DragDropContextWrapper>
</TestProviders>
);
expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data);
});
test('if in tgrid expanded value, it invokes `renderColumn` with the expected arguments', () => {
const data = cloneDeep(mockTimelineData[0].data);
const header = cloneDeep(defaultHeaders[0]);
const isDetails = true;
const truncate = isDetails ? false : true;
mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper droppableId="testing">
<PreviewTableCellRenderer
browserFields={browserFields}
columnId={columnId}
data={data}
ecsData={ecsData}
eventId={eventId}
header={header}
isDetails={isDetails}
isDraggable={true}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
setCellProps={setCellProps}
timelineId={timelineId}
truncate={truncate}
/>
</DroppableWrapper>
</DragDropContextWrapper>
</TestProviders>
);
expect(mockImplementation.renderColumn).toBeCalledWith({
asPlainText: false,
browserFields,
columnName: header.id,
ecsData,
eventId,
field: header,
isDetails,
isDraggable: true,
linkValues,
rowRenderers: undefined,
timelineId,
truncate,
values: ['2018-11-05T19:03:25.937Z'],
});
});
test('if in tgrid expanded value, it does not render any actions', () => {
const data = cloneDeep(mockTimelineData[0].data);
const header = cloneDeep(defaultHeaders[1]);
const isDetails = true;
const id = 'event.severity';
const wrapper = mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DroppableWrapper droppableId="testing">
<PreviewTableCellRenderer
browserFields={browserFields}
columnId={id}
ecsData={ecsData}
data={data}
eventId={eventId}
header={header}
isDetails={isDetails}
isDraggable={true}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues}
rowIndex={rowIndex}
colIndex={colIndex}
setCellProps={setCellProps}
timelineId={timelineId}
/>
</DroppableWrapper>
</DragDropContextWrapper>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists()
).toBeFalsy();
});
});

View file

@ -89,6 +89,7 @@ export const PreviewTableCellRenderer: React.FC<CellValueElementProps> = ({
const styledContentClassName = isDetails
? 'eui-textBreakWord'
: 'eui-displayInlineBlock eui-textTruncate';
return (
<>
<StyledContent className={styledContentClassName} $isDetails={isDetails}>

View file

@ -54,7 +54,7 @@ export const QUERY_PREVIEW_LABEL = i18n.translate(
export const QUERY_PREVIEW_HELP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText',
{
defaultMessage: 'Select a timeframe of data to preview query results',
defaultMessage: 'Select a timeframe of data to preview query results.',
}
);
@ -73,9 +73,9 @@ export const THRESHOLD_QUERY_GRAPH_COUNT = i18n.translate(
);
export const QUERY_GRAPH_HITS_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle',
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewTitle',
{
defaultMessage: 'Hits',
defaultMessage: 'Rule Preview',
}
);
@ -124,18 +124,25 @@ export const QUERY_PREVIEW_ERROR = i18n.translate(
);
export const QUERY_PREVIEW_DISCLAIMER = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer',
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer',
{
defaultMessage:
'Note: This preview excludes effects of rule exceptions and timestamp overrides.',
}
);
export const QUERY_PREVIEW_DISCLAIMER_MAX_SIGNALS = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql',
export const PREVIEW_HISTOGRAM_DISCLAIMER = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer',
{
defaultMessage:
'Note: This preview excludes effects of rule exceptions and timestamp overrides, and is limited to 100 results.',
'Note: Alerts with multiple event.category values will be counted more than once.',
}
);
export const ML_PREVIEW_HISTOGRAM_DISCLAIMER = i18n.translate(
'xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer',
{
defaultMessage: 'Note: Alerts with multiple host.name values will be counted more than once.',
}
);

View file

@ -13,14 +13,12 @@ import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/commo
import { useKibana } from '../../../../common/lib/kibana';
import { QUERY_PREVIEW_ERROR } from './translations';
import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
import { FieldValueThreshold } from '../threshold_input';
interface PreviewHistogramParams {
previewId: string | undefined;
endDate: string;
startDate: string;
spaceId: string;
threshold?: FieldValueThreshold;
index: string[];
ruleType: Type;
}
@ -30,7 +28,6 @@ export const usePreviewHistogram = ({
startDate,
endDate,
spaceId,
threshold,
index,
ruleType,
}: PreviewHistogramParams) => {
@ -47,9 +44,8 @@ export const usePreviewHistogram = ({
});
const stackByField = useMemo(() => {
const stackByDefault = ruleType === 'machine_learning' ? 'host.name' : 'event.category';
return threshold?.field[0] ?? stackByDefault;
}, [threshold, ruleType]);
return ruleType === 'machine_learning' ? 'host.name' : 'event.category';
}, [ruleType]);
const matrixHistogramRequest = useMemo(() => {
return {
@ -60,11 +56,10 @@ export const usePreviewHistogram = ({
indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`],
stackByField,
startDate,
threshold,
includeMissingData: false,
skip: error != null,
};
}, [startDate, endDate, filterQuery, spaceId, error, threshold, stackByField]);
}, [startDate, endDate, filterQuery, spaceId, error, stackByField]);
return useMatrixHistogramCombined(matrixHistogramRequest);
};

View file

@ -16,25 +16,9 @@ import {
import * as i18n from '../../components/alerts_table/translations';
/**
* columns implements a subset of `EuiDataGrid`'s `EuiDataGridColumn` interface,
* plus additional TGrid column properties
*/
export const columns: Array<
const baseColumns: Array<
Pick<EuiDataGridColumn, 'display' | 'displayAsText' | 'id' | 'initialWidth'> & ColumnHeaderOptions
> = [
{
columnHeaderType: defaultColumnHeaderType,
id: '@timestamp',
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH + 10,
},
{
columnHeaderType: defaultColumnHeaderType,
displayAsText: i18n.ALERTS_HEADERS_RULE,
id: 'kibana.alert.rule.name',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
linkField: 'kibana.alert.rule.uuid',
},
{
columnHeaderType: defaultColumnHeaderType,
displayAsText: i18n.ALERTS_HEADERS_SEVERITY,
@ -78,3 +62,36 @@ export const columns: Array<
id: 'destination.ip',
},
];
/**
* columns implements a subset of `EuiDataGrid`'s `EuiDataGridColumn` interface,
* plus additional TGrid column properties
*/
export const columns: Array<
Pick<EuiDataGridColumn, 'display' | 'displayAsText' | 'id' | 'initialWidth'> & ColumnHeaderOptions
> = [
{
columnHeaderType: defaultColumnHeaderType,
id: '@timestamp',
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH + 10,
},
{
columnHeaderType: defaultColumnHeaderType,
displayAsText: i18n.ALERTS_HEADERS_RULE,
id: 'kibana.alert.rule.name',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
linkField: 'kibana.alert.rule.uuid',
},
...baseColumns,
];
export const rulePreviewColumns: Array<
Pick<EuiDataGridColumn, 'display' | 'displayAsText' | 'id' | 'initialWidth'> & ColumnHeaderOptions
> = [
{
columnHeaderType: defaultColumnHeaderType,
id: 'kibana.alert.original_time',
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH + 10,
},
...baseColumns,
];

View file

@ -167,4 +167,13 @@ describe('event details footer component', () => {
);
expect(wrapper.getByTestId('side-panel-flyout-footer')).toBeTruthy();
});
test("it doesn't render the take action dropdown when readOnly prop is passed", () => {
const wrapper = render(
<TestProviders>
<EventDetailsPanel {...{ ...defaultProps, isReadOnly: true }} isFlyoutView={true} />
</TestProviders>
);
const element = wrapper.queryByTestId('side-panel-flyout-footer');
expect(element).toBeNull();
});
});

View file

@ -322,6 +322,7 @@ export enum TimelineId {
casePage = 'timeline-case',
test = 'test', // Reserved for testing purposes
alternateTest = 'alternateTest',
rulePreview = 'rule-preview',
}
export const TimelineIdLiteralRt = runtimeTypes.union([
@ -333,6 +334,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([
runtimeTypes.literal(TimelineId.networkPageExternalAlerts),
runtimeTypes.literal(TimelineId.active),
runtimeTypes.literal(TimelineId.test),
runtimeTypes.literal(TimelineId.rulePreview),
]);
export type TimelineIdLiteral = runtimeTypes.TypeOf<typeof TimelineIdLiteralRt>;

View file

@ -113,7 +113,7 @@ export interface TGridIntegratedProps {
filterStatus?: AlertStatus;
globalFullScreen: boolean;
// If truthy, the graph viewer (Resolver) is showing
graphEventId: string | undefined;
graphEventId?: string;
graphOverlay?: React.ReactNode;
hasAlertsCrud: boolean;
height?: number;

View file

@ -20726,9 +20726,6 @@
"xpack.securitySolution.detectionEngine.pageTitle": "Moteur de détection",
"xpack.securitySolution.detectionEngine.panelSubtitleShowing": "Affichant",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "Compte",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "Remarque : cet aperçu exclut les effets d'exceptions aux règles et les remplacements d'horodatages.",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "Remarque : cet aperçu exclut les effets d'exceptions aux règles et les remplacements d'horodatages, et est limité à 100 résultats.",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "Résultats",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "Erreur de récupération de l'aperçu",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "aperçu de la recherche",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "Avertissement de bruit : cette règle peut générer beaucoup de bruit. Envisagez d'affiner votre recherche. La base est une progression linéaire comportant 1 alerte par heure.",

View file

@ -23586,9 +23586,6 @@
"xpack.securitySolution.detectionEngine.pageTitle": "検出エンジン",
"xpack.securitySolution.detectionEngine.panelSubtitleShowing": "表示中",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "カウント",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "注:このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "注このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。結果は100件に制限されます。",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "ヒット数",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "プレビュー取得エラー",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "クエリプレビュー",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "イズ警告このルールではイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。",

View file

@ -23613,9 +23613,6 @@
"xpack.securitySolution.detectionEngine.pageTitle": "检测引擎",
"xpack.securitySolution.detectionEngine.panelSubtitleShowing": "正在显示",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "计数",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "注意:此预览不包括规则例外和时间戳覆盖的影响。",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "注意:此预览不包括规则例外和时间戳覆盖的影响,且仅显示 100 个结果。",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "命中数",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "提取预览时出错",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "查询预览",
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。",

View file

@ -116,12 +116,12 @@ export const getSimpleRule = (ruleId = 'rule-1', enabled = false): QueryCreateSc
/**
* This is a typical simple preview rule for testing that is easy for most basic testing
* @param ruleId
* @param enabled The number of times the rule will be run through the executors. Defaulted to 20,
* @param enabled The number of times the rule will be run through the executors. Defaulted to 12,
* the execution time for the default interval time of 5m.
*/
export const getSimplePreviewRule = (
ruleId = 'preview-rule-1',
invocationCount = 20
invocationCount = 12
): PreviewRulesSchema => ({
name: 'Simple Rule Query',
description: 'Simple Rule Query',