mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
### Summary This PR addresses the remaining query preview bugs. - it adds index, and request information to eql inspect - it seems that for some reason the eql search strategy response returns `null` for the `params.body` in complete responses, but not in partial responses and does not include index info. As a workaround, I set the inspect info on partial responses and manually add index info - added to-dos pointing this out in the code - updated eql sequence queries preview to use the last event timestamp of a sequence to display the hits within a histogram - it checks buckets length to determine noise warning for threshold rules, as opposed to total hit count - remove unused i18n text - fixes bug where threshold is being passed in for all rule types as it's always defined in the creation step, added a check to only pass through to `useMatrixHistogram` hook when rule type is threshold
This commit is contained in:
parent
b034db9ed7
commit
a92a3d1af7
19 changed files with 1186 additions and 590 deletions
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
|
||||
import { Source } from './types';
|
||||
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||
import { Connection } from '@elastic/elasticsearch';
|
||||
|
||||
export const getMockEqlResponse = (): EqlSearchStrategyResponse<EqlSearchResponse<Source>> => ({
|
||||
id: 'some-id',
|
||||
rawResponse: {
|
||||
body: {
|
||||
hits: {
|
||||
events: [
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '1',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:16:54.368707900Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '2',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:50:54.368707900Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '3',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:06:54.368707900Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '4',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:15:54.368707900Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
total: {
|
||||
value: 4,
|
||||
relation: '',
|
||||
},
|
||||
},
|
||||
is_partial: false,
|
||||
is_running: false,
|
||||
took: 300,
|
||||
timed_out: false,
|
||||
},
|
||||
headers: {},
|
||||
warnings: [],
|
||||
meta: {
|
||||
aborted: false,
|
||||
attempts: 0,
|
||||
context: null,
|
||||
name: 'elasticsearch-js',
|
||||
connection: {} as Connection,
|
||||
request: {
|
||||
params: {
|
||||
body: JSON.stringify({
|
||||
filter: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2020-10-07T00:46:12.414Z',
|
||||
lte: '2020-10-07T01:46:12.414Z',
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
method: 'GET',
|
||||
path: '/_eql/search/',
|
||||
querystring: 'some query string',
|
||||
},
|
||||
options: {},
|
||||
id: '',
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
},
|
||||
});
|
||||
|
||||
export const getMockEqlSequenceResponse = (): EqlSearchStrategyResponse<
|
||||
EqlSearchResponse<Source>
|
||||
> => ({
|
||||
id: 'some-id',
|
||||
rawResponse: {
|
||||
body: {
|
||||
hits: {
|
||||
sequences: [
|
||||
{
|
||||
join_keys: [],
|
||||
events: [
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '1',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:16:54.368707900Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '2',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:50:54.368707900Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
join_keys: [],
|
||||
events: [
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '3',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:06:54.368707900Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
_index: 'index',
|
||||
_id: '4',
|
||||
_source: {
|
||||
'@timestamp': '2020-10-04T15:15:54.368707900Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
total: {
|
||||
value: 4,
|
||||
relation: '',
|
||||
},
|
||||
},
|
||||
is_partial: false,
|
||||
is_running: false,
|
||||
took: 300,
|
||||
timed_out: false,
|
||||
},
|
||||
headers: {},
|
||||
warnings: [],
|
||||
meta: {
|
||||
aborted: false,
|
||||
attempts: 0,
|
||||
context: null,
|
||||
name: 'elasticsearch-js',
|
||||
connection: {} as Connection,
|
||||
request: {
|
||||
params: {
|
||||
body: JSON.stringify({
|
||||
filter: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2020-10-07T00:46:12.414Z',
|
||||
lte: '2020-10-07T01:46:12.414Z',
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
method: 'GET',
|
||||
path: '/_eql/search/',
|
||||
querystring: 'some query string',
|
||||
},
|
||||
options: {},
|
||||
id: '',
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
},
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -10,55 +10,105 @@ import { inputsModel } from '../../../common/store';
|
|||
import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common';
|
||||
import { InspectResponse } from '../../../types';
|
||||
import { EqlPreviewResponse, Source } from './types';
|
||||
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||
import { BaseHit, EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||
|
||||
type EqlAggBuckets = Record<string, { timestamp: string; total: number }>;
|
||||
|
||||
export const EQL_QUERY_EVENT_SIZE = 100;
|
||||
|
||||
// Calculates which 2 min bucket segment, event should be
|
||||
// sorted into
|
||||
/**
|
||||
* Calculates which 2 min bucket segment, event should be sorted into
|
||||
* @param eventTimestamp The event to be bucketed timestamp
|
||||
* @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred
|
||||
*/
|
||||
export const calculateBucketForHour = (eventTimestamp: number, relativeNow: number): number => {
|
||||
const diff: number = relativeNow - eventTimestamp;
|
||||
const minutes: number = Math.floor(diff / 60000);
|
||||
const diff = Math.abs(relativeNow - eventTimestamp);
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
return Math.ceil(minutes / 2) * 2;
|
||||
};
|
||||
|
||||
// Calculates which 1 hour bucket segment, event should be
|
||||
// sorted into
|
||||
/**
|
||||
* Calculates which 1 hour bucket segment, event should be sorted into
|
||||
* @param eventTimestamp The event to be bucketed timestamp
|
||||
* @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred
|
||||
*/
|
||||
export const calculateBucketForDay = (eventTimestamp: number, relativeNow: number): number => {
|
||||
const diff: number = relativeNow - eventTimestamp;
|
||||
const minutes: number = Math.floor(diff / 60000);
|
||||
const diff = Math.abs(relativeNow - eventTimestamp);
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
return Math.ceil(minutes / 60);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the response for the UI inspect modal
|
||||
* @param response The query search response
|
||||
* @param indices The indices the query searched
|
||||
* TODO: Update eql search strategy to return index in it's meta
|
||||
* params info, currently not being returned, but expected for
|
||||
* inspect modal display
|
||||
*/
|
||||
export const formatInspect = (
|
||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>
|
||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>,
|
||||
indices: string[]
|
||||
): InspectResponse => {
|
||||
const body = response.rawResponse.meta.request.params.body;
|
||||
const bodyParse = typeof body === 'string' ? JSON.parse(body) : body;
|
||||
const bodyParse: Record<string, unknown> | undefined =
|
||||
typeof body === 'string' ? JSON.parse(body) : body;
|
||||
return {
|
||||
dsl: [
|
||||
JSON.stringify({ ...response.rawResponse.meta.request.params, body: bodyParse }, null, 2),
|
||||
JSON.stringify(
|
||||
{ ...response.rawResponse.meta.request.params, index: indices, body: bodyParse },
|
||||
null,
|
||||
2
|
||||
),
|
||||
],
|
||||
response: [JSON.stringify(response.rawResponse.body, null, 2)],
|
||||
};
|
||||
};
|
||||
|
||||
// NOTE: Eql does not support aggregations, this is an in-memory
|
||||
// hand-spun aggregation for the events to give the user a visual
|
||||
// representation of their query results
|
||||
/**
|
||||
* Gets the events out of the response based on type of query
|
||||
* @param isSequence Is the eql query a sequence query
|
||||
* @param response The query search response
|
||||
*/
|
||||
export const getEventsToBucket = (
|
||||
isSequence: boolean,
|
||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>
|
||||
): Array<BaseHit<Source>> => {
|
||||
const hits = response.rawResponse.body.hits ?? [];
|
||||
if (isSequence) {
|
||||
return (
|
||||
hits.sequences?.map((seq) => {
|
||||
return seq.events[seq.events.length - 1];
|
||||
}) ?? []
|
||||
);
|
||||
} else {
|
||||
return hits.events ?? [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Eql does not support aggregations, this is an in-memory
|
||||
* hand-spun aggregation for the events to give the user a visual
|
||||
* representation of their query results
|
||||
* @param response The query search response
|
||||
* @param range User chosen timeframe (last hour, day)
|
||||
* @param to Based on range chosen
|
||||
* @param refetch Callback used in inspect button, ref just passed through
|
||||
* @param indices Indices searched by query
|
||||
* @param isSequence Is the eql query a sequence query
|
||||
*/
|
||||
export const getEqlAggsData = (
|
||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>,
|
||||
range: Unit,
|
||||
to: string,
|
||||
refetch: inputsModel.Refetch
|
||||
refetch: inputsModel.Refetch,
|
||||
indices: string[],
|
||||
isSequence: boolean
|
||||
): EqlPreviewResponse => {
|
||||
const { dsl, response: inspectResponse } = formatInspect(response);
|
||||
// The upper bound of the timestamps
|
||||
const { dsl, response: inspectResponse } = formatInspect(response, indices);
|
||||
const relativeNow = Date.parse(to);
|
||||
const accumulator = getInterval(range, relativeNow);
|
||||
const events = response.rawResponse.body.hits.events ?? [];
|
||||
const events = getEventsToBucket(isSequence, response);
|
||||
const totalCount = response.rawResponse.body.hits.total.value;
|
||||
|
||||
const buckets = events.reduce<EqlAggBuckets>((acc, hit) => {
|
||||
|
@ -94,12 +144,23 @@ export const getEqlAggsData = (
|
|||
};
|
||||
};
|
||||
|
||||
export const createIntervalArray = (start: number, end: number, multiplier: number) => {
|
||||
/**
|
||||
* Helper method to create an array to be used for calculating bucket intervals
|
||||
* @param start
|
||||
* @param end
|
||||
* @param multiplier
|
||||
*/
|
||||
export const createIntervalArray = (start: number, end: number, multiplier: number): number[] => {
|
||||
return Array(end - start + 1)
|
||||
.fill(0)
|
||||
.map((_, idx) => start + idx * multiplier);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method to create an array to be used for calculating bucket intervals
|
||||
* @param range User chosen timeframe (last hour, day)
|
||||
* @param relativeNow Based on range chosen
|
||||
*/
|
||||
export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets => {
|
||||
switch (range) {
|
||||
case 'h':
|
||||
|
@ -117,38 +178,6 @@ export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets =>
|
|||
};
|
||||
}, {});
|
||||
default:
|
||||
throw new Error('Invalid time range selected');
|
||||
throw new RangeError('Invalid time range selected. Must be "Last hour" or "Last day".');
|
||||
}
|
||||
};
|
||||
|
||||
export const getSequenceAggs = (
|
||||
response: EqlSearchStrategyResponse<EqlSearchResponse<Source>>,
|
||||
refetch: inputsModel.Refetch
|
||||
): EqlPreviewResponse => {
|
||||
const { dsl, response: inspectResponse } = formatInspect(response);
|
||||
const sequences = response.rawResponse.body.hits.sequences ?? [];
|
||||
const totalCount = response.rawResponse.body.hits.total.value;
|
||||
|
||||
const data = sequences.map((sequence, i) => {
|
||||
return sequence.events.map((seqEvent) => {
|
||||
if (seqEvent._source['@timestamp'] == null) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
x: seqEvent._source['@timestamp'],
|
||||
y: 1,
|
||||
g: `Seq. ${i + 1}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
data: data.flat(),
|
||||
totalCount,
|
||||
inspect: {
|
||||
dsl,
|
||||
response: inspectResponse,
|
||||
},
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Source } from './types';
|
|||
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useEqlPreview } from '.';
|
||||
import { getMockResponse } from './helpers.test';
|
||||
import { getMockEqlResponse } from './eql_search_response.mock';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
|
@ -32,7 +32,9 @@ describe('useEqlPreview', () => {
|
|||
|
||||
useKibana().services.notifications.toasts.addWarning = jest.fn();
|
||||
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse()));
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
);
|
||||
});
|
||||
|
||||
it('should initiate hook', async () => {
|
||||
|
@ -96,7 +98,7 @@ describe('useEqlPreview', () => {
|
|||
it('should not resolve values after search is invoked if component unmounted', async () => {
|
||||
await act(async () => {
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockResponse()).pipe(delay(5000))
|
||||
of(getMockEqlResponse()).pipe(delay(5000))
|
||||
);
|
||||
const { result, waitForNextUpdate, unmount } = renderHook(() => useEqlPreview());
|
||||
|
||||
|
@ -117,9 +119,11 @@ describe('useEqlPreview', () => {
|
|||
it('should not resolve new values on search if response is error response', async () => {
|
||||
await act(async () => {
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of({ isRunning: false, isPartial: true } as EqlSearchStrategyResponse<
|
||||
EqlSearchResponse<Source>
|
||||
>)
|
||||
of<EqlSearchStrategyResponse<EqlSearchResponse<Source>>>({
|
||||
...getMockEqlResponse(),
|
||||
isRunning: false,
|
||||
isPartial: true,
|
||||
})
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useEqlPreview());
|
||||
|
@ -136,6 +140,30 @@ describe('useEqlPreview', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO: Determine why eql search strategy returns null for meta.params.body
|
||||
// in complete responses, but not in partial responses
|
||||
it('should update inspect information on partial response', async () => {
|
||||
const mockResponse = getMockEqlResponse();
|
||||
await act(async () => {
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of<EqlSearchStrategyResponse<EqlSearchResponse<Source>>>({
|
||||
isRunning: true,
|
||||
isPartial: true,
|
||||
rawResponse: mockResponse.rawResponse,
|
||||
})
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useEqlPreview());
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current[1](params);
|
||||
|
||||
expect(result.current[2].inspect.dsl.length).toEqual(1);
|
||||
expect(result.current[2].inspect.response.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add danger toast if search throws', async () => {
|
||||
await act(async () => {
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
|
|
|
@ -14,12 +14,13 @@ import {
|
|||
AbortError,
|
||||
isCompleteResponse,
|
||||
isErrorResponse,
|
||||
isPartialResponse,
|
||||
} from '../../../../../../../src/plugins/data/common';
|
||||
import {
|
||||
EqlSearchStrategyRequest,
|
||||
EqlSearchStrategyResponse,
|
||||
} from '../../../../../data_enhanced/common';
|
||||
import { getEqlAggsData, getSequenceAggs } from './helpers';
|
||||
import { formatInspect, getEqlAggsData } from './helpers';
|
||||
import { EqlPreviewResponse, EqlPreviewRequest, Source } from './types';
|
||||
import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils';
|
||||
import { EqlSearchResponse } from '../../../../common/detection_engine/types';
|
||||
|
@ -106,13 +107,37 @@ export const useEqlPreview = (): [
|
|||
if (isCompleteResponse(res)) {
|
||||
if (!didCancel.current) {
|
||||
setLoading(false);
|
||||
if (hasEqlSequenceQuery(query)) {
|
||||
setResponse(getSequenceAggs(res, refetch.current));
|
||||
} else {
|
||||
setResponse(getEqlAggsData(res, interval, to, refetch.current));
|
||||
}
|
||||
|
||||
setResponse((prev) => {
|
||||
const { inspect, ...rest } = getEqlAggsData(
|
||||
res,
|
||||
interval,
|
||||
to,
|
||||
refetch.current,
|
||||
index,
|
||||
hasEqlSequenceQuery(query)
|
||||
);
|
||||
const inspectDsl = prev.inspect.dsl[0] ? prev.inspect.dsl : inspect.dsl;
|
||||
const inspectResp = prev.inspect.response[0]
|
||||
? prev.inspect.response
|
||||
: inspect.response;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
...rest,
|
||||
inspect: {
|
||||
dsl: inspectDsl,
|
||||
response: inspectResp,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribeStream.current.next();
|
||||
} else if (isPartialResponse(res)) {
|
||||
// TODO: Eql search strategy partial responses return a value under meta.params.body
|
||||
// but the final/complete response does not, that's why the inspect values are set here
|
||||
setResponse((prev) => ({ ...prev, inspect: formatInspect(res, index) }));
|
||||
} else if (isErrorResponse(res)) {
|
||||
setLoading(false);
|
||||
notifications.toasts.addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE);
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('PreviewCustomQueryHistogram', () => {
|
|||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle')
|
||||
).toEqual(i18n.PREVIEW_SUBTITLE_LOADING);
|
||||
).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
|
||||
});
|
||||
|
||||
test('it configures data and subtitle', () => {
|
||||
|
|
|
@ -54,7 +54,7 @@ export const PreviewCustomQueryHistogram = ({
|
|||
|
||||
const subtitle = useMemo(
|
||||
(): string =>
|
||||
isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
[isLoading, totalCount]
|
||||
);
|
||||
|
||||
|
@ -67,7 +67,7 @@ export const PreviewCustomQueryHistogram = ({
|
|||
barConfig={barConfig}
|
||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||
subtitle={subtitle}
|
||||
disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER}
|
||||
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
|
||||
isLoading={isLoading}
|
||||
data-test-subj="queryPreviewCustomHistogram"
|
||||
/>
|
||||
|
|
|
@ -39,7 +39,6 @@ describe('PreviewEqlQueryHistogram', () => {
|
|||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
query="file where true"
|
||||
data={[]}
|
||||
totalCount={0}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
|
@ -53,7 +52,7 @@ describe('PreviewEqlQueryHistogram', () => {
|
|||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle')
|
||||
).toEqual(i18n.PREVIEW_SUBTITLE_LOADING);
|
||||
).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING);
|
||||
});
|
||||
|
||||
test('it configures data and subtitle', () => {
|
||||
|
@ -63,7 +62,6 @@ describe('PreviewEqlQueryHistogram', () => {
|
|||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
query="file where true"
|
||||
data={[
|
||||
{ x: 1602247050000, y: 2314, g: 'All others' },
|
||||
{ x: 1602247162500, y: 3471, g: 'All others' },
|
||||
|
@ -115,7 +113,6 @@ describe('PreviewEqlQueryHistogram', () => {
|
|||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
query="file where true"
|
||||
data={[]}
|
||||
totalCount={0}
|
||||
inspect={{ dsl: ['some dsl'], response: ['query response'] }}
|
||||
|
@ -133,4 +130,32 @@ describe('PreviewEqlQueryHistogram', () => {
|
|||
refetch: mockRefetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('it displays histogram', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<TestProviders>
|
||||
<PreviewEqlQueryHistogram
|
||||
to="2020-07-08T08:20:18.966Z"
|
||||
from="2020-07-07T08:20:18.966Z"
|
||||
data={[
|
||||
{ x: 1602247050000, y: 2314, g: 'All others' },
|
||||
{ x: 1602247162500, y: 3471, g: 'All others' },
|
||||
{ x: 1602247275000, y: 3369, g: 'All others' },
|
||||
]}
|
||||
totalCount={9154}
|
||||
inspect={{ dsl: [], response: [] }}
|
||||
refetch={jest.fn()}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="sharedPreviewQueryNoHistogramAvailable"]').exists()
|
||||
).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from '../../../../common/components/charts/common';
|
||||
import { InspectQuery } from '../../../../common/store/inputs/model';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { hasEqlSequenceQuery } from '../../../../../common/detection_engine/utils';
|
||||
import { inputsModel } from '../../../../common/store';
|
||||
import { PreviewHistogram } from './histogram';
|
||||
|
||||
|
@ -26,7 +25,6 @@ interface PreviewEqlQueryHistogramProps {
|
|||
from: string;
|
||||
totalCount: number;
|
||||
isLoading: boolean;
|
||||
query: string;
|
||||
data: ChartData[];
|
||||
inspect: InspectQuery;
|
||||
refetch: inputsModel.Refetch;
|
||||
|
@ -36,7 +34,6 @@ export const PreviewEqlQueryHistogram = ({
|
|||
from,
|
||||
to,
|
||||
totalCount,
|
||||
query,
|
||||
data,
|
||||
inspect,
|
||||
refetch,
|
||||
|
@ -50,14 +47,11 @@ export const PreviewEqlQueryHistogram = ({
|
|||
}
|
||||
}, [setQuery, inspect, isInitializing, refetch]);
|
||||
|
||||
const barConfig = useMemo(
|
||||
(): ChartSeriesConfigs => getHistogramConfig(to, from, hasEqlSequenceQuery(query)),
|
||||
[from, to, query]
|
||||
);
|
||||
const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]);
|
||||
|
||||
const subtitle = useMemo(
|
||||
(): string =>
|
||||
isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount),
|
||||
[isLoading, totalCount]
|
||||
);
|
||||
|
||||
|
@ -70,7 +64,7 @@ export const PreviewEqlQueryHistogram = ({
|
|||
barConfig={barConfig}
|
||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||
subtitle={subtitle}
|
||||
disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER_EQL}
|
||||
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER_EQL}
|
||||
isLoading={isLoading}
|
||||
data-test-subj="queryPreviewEqlHistogram"
|
||||
/>
|
||||
|
|
|
@ -16,13 +16,13 @@ import { FieldValueQueryBar } from '../query_bar';
|
|||
import { ESQuery } from '../../../../../common/typed_json';
|
||||
import { Filter } from '../../../../../../../../src/plugins/data/common/es_query';
|
||||
|
||||
export const HITS_THRESHOLD: Record<string, number> = {
|
||||
h: 1,
|
||||
d: 24,
|
||||
M: 730,
|
||||
};
|
||||
|
||||
export const isNoisy = (hits: number, timeframe: Unit) => {
|
||||
/**
|
||||
* Determines whether or not to display noise warning.
|
||||
* Is considered noisy if alerts/hour rate > 1
|
||||
* @param hits Total query search hits
|
||||
* @param timeframe Range selected by user (last hour, day...)
|
||||
*/
|
||||
export const isNoisy = (hits: number, timeframe: Unit): boolean => {
|
||||
if (timeframe === 'h') {
|
||||
return hits > 1;
|
||||
} else if (timeframe === 'd') {
|
||||
|
@ -34,6 +34,12 @@ export const isNoisy = (hits: number, timeframe: Unit) => {
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines what timerange options to show.
|
||||
* Eql sequence queries tend to be slower, so decided
|
||||
* not to include the last month option.
|
||||
* @param ruleType
|
||||
*/
|
||||
export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
|
||||
if (ruleType === 'eql') {
|
||||
return [
|
||||
|
@ -49,6 +55,13 @@ export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Quick little helper to extract the query info from the
|
||||
* queryBar object.
|
||||
* @param queryBar Object containing all query info
|
||||
* @param index Indices searched
|
||||
* @param ruleType
|
||||
*/
|
||||
export const getInfoFromQueryBar = (
|
||||
queryBar: FieldValueQueryBar,
|
||||
index: string[],
|
||||
|
@ -88,10 +101,15 @@ export const getInfoFromQueryBar = (
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Config passed into elastic-charts settings.
|
||||
* @param to
|
||||
* @param from
|
||||
*/
|
||||
export const getHistogramConfig = (
|
||||
to: string,
|
||||
from: string,
|
||||
showLegend: boolean = false
|
||||
showLegend = false
|
||||
): ChartSeriesConfigs => {
|
||||
return {
|
||||
series: {
|
||||
|
@ -131,7 +149,11 @@ export const getHistogramConfig = (
|
|||
};
|
||||
};
|
||||
|
||||
export const getThresholdHistogramConfig = (height: number | undefined): ChartSeriesConfigs => {
|
||||
/**
|
||||
* Threshold histogram is displayed a bit differently,
|
||||
* x-axis is not time based, but ordinal.
|
||||
*/
|
||||
export const getThresholdHistogramConfig = (): ChartSeriesConfigs => {
|
||||
return {
|
||||
series: {
|
||||
xScaleType: ScaleType.Ordinal,
|
||||
|
@ -165,6 +187,6 @@ export const getThresholdHistogramConfig = (height: number | undefined): ChartSe
|
|||
},
|
||||
},
|
||||
},
|
||||
customHeight: height ?? 200,
|
||||
customHeight: 200,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,9 +13,13 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
|||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { PreviewQuery } from './';
|
||||
import { getMockResponse } from '../../../../common/hooks/eql/helpers.test';
|
||||
import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_response.mock';
|
||||
import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram';
|
||||
import { useEqlPreview } from '../../../../common/hooks/eql/';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/containers/matrix_histogram');
|
||||
jest.mock('../../../../common/hooks/eql/');
|
||||
|
||||
describe('PreviewQuery', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -23,7 +27,33 @@ describe('PreviewQuery', () => {
|
|||
|
||||
useKibana().services.notifications.toasts.addWarning = jest.fn();
|
||||
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse()));
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 1,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
(useEqlPreview as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 1,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -121,6 +151,42 @@ describe('PreviewQuery', () => {
|
|||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'host.name:*', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 2,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders query histogram when rule type is saved_query and preview button clicked', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
|
@ -175,6 +241,42 @@ describe('PreviewQuery', () => {
|
|||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="eql"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
(useEqlPreview as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 2,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [],
|
||||
},
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
|
@ -192,16 +294,70 @@ describe('PreviewQuery', () => {
|
|||
</ThemeProvider>
|
||||
);
|
||||
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 500,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [{ key: 'siem-kibana', doc_count: 500 }],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
|
||||
|
||||
expect(mockCalls.length).toEqual(1);
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="query"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={{ field: 'agent.hostname', value: 200 }}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
(useMatrixHistogram as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
{
|
||||
inspect: { dsl: [], response: [] },
|
||||
totalCount: 500,
|
||||
refetch: jest.fn(),
|
||||
data: [],
|
||||
buckets: [
|
||||
{ key: 'siem-kibana', doc_count: 200 },
|
||||
{ key: 'siem-windows', doc_count: 300 },
|
||||
],
|
||||
},
|
||||
(useKibana().services.data.search.search as jest.Mock).mockReturnValue(
|
||||
of(getMockEqlResponse())
|
||||
),
|
||||
]);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
|
@ -255,4 +411,33 @@ describe('PreviewQuery', () => {
|
|||
expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it hides histogram when timeframe changes', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
|
||||
<TestProviders>
|
||||
<PreviewQuery
|
||||
ruleType="threshold"
|
||||
dataTestSubj="queryPreviewSelect"
|
||||
idAria="queryPreview"
|
||||
query={{ query: { query: 'file where true', language: 'kuery' }, filters: [] }}
|
||||
index={['foo-*']}
|
||||
threshold={undefined}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy();
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="queryPreviewTimeframeSelect"] select')
|
||||
.at(0)
|
||||
.simulate('change', { target: { value: 'd' } });
|
||||
|
||||
expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, useCallback, useEffect, useReducer } from 'react';
|
||||
import React, { Fragment, useCallback, useEffect, useReducer, useRef } from 'react';
|
||||
import { Unit } from '@elastic/datemath';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
|
@ -110,29 +110,31 @@ export const PreviewQuery = ({
|
|||
{ inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets },
|
||||
startNonEql,
|
||||
] = useMatrixHistogram({
|
||||
errorMessage: i18n.PREVIEW_QUERY_ERROR,
|
||||
errorMessage: i18n.QUERY_PREVIEW_ERROR,
|
||||
endDate: fromTime,
|
||||
startDate: toTime,
|
||||
filterQuery: queryFilter,
|
||||
indexNames: index,
|
||||
histogramType: MatrixHistogramType.events,
|
||||
stackByField: 'event.category',
|
||||
threshold,
|
||||
threshold: ruleType === 'threshold' ? threshold : undefined,
|
||||
skip: true,
|
||||
});
|
||||
|
||||
const setQueryInfo = useCallback(
|
||||
(queryBar: FieldValueQueryBar | undefined): void => {
|
||||
(queryBar: FieldValueQueryBar | undefined, indices: string[], type: Type): void => {
|
||||
dispatch({
|
||||
type: 'setQueryInfo',
|
||||
queryBar,
|
||||
index,
|
||||
ruleType,
|
||||
index: indices,
|
||||
ruleType: type,
|
||||
});
|
||||
},
|
||||
[dispatch, index, ruleType]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const debouncedSetQueryInfo = useRef(debounce(500, setQueryInfo));
|
||||
|
||||
const setTimeframeSelect = useCallback(
|
||||
(selection: Unit): void => {
|
||||
dispatch({
|
||||
|
@ -190,11 +192,9 @@ export const PreviewQuery = ({
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
const debounced = debounce(1000, setQueryInfo);
|
||||
|
||||
debounced(query);
|
||||
}, [setQueryInfo, query]);
|
||||
useEffect(() => {
|
||||
debouncedSetQueryInfo.current(query, index, ruleType);
|
||||
}, [index, query, ruleType]);
|
||||
|
||||
useEffect((): void => {
|
||||
setThresholdValues(threshold, ruleType);
|
||||
|
@ -205,12 +205,32 @@ export const PreviewQuery = ({
|
|||
}, [ruleType, setRuleTypeChange]);
|
||||
|
||||
useEffect((): void => {
|
||||
const totalHits = ruleType === 'eql' ? eqlQueryTotal : matrixHistTotal;
|
||||
|
||||
if (isNoisy(totalHits, timeframe)) {
|
||||
setNoiseWarning();
|
||||
switch (ruleType) {
|
||||
case 'eql':
|
||||
if (isNoisy(eqlQueryTotal, timeframe)) {
|
||||
setNoiseWarning();
|
||||
}
|
||||
break;
|
||||
case 'threshold':
|
||||
const totalHits = thresholdFieldExists ? buckets.length : matrixHistTotal;
|
||||
if (isNoisy(totalHits, timeframe)) {
|
||||
setNoiseWarning();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (isNoisy(matrixHistTotal, timeframe)) {
|
||||
setNoiseWarning();
|
||||
}
|
||||
}
|
||||
}, [timeframe, matrixHistTotal, eqlQueryTotal, ruleType, setNoiseWarning]);
|
||||
}, [
|
||||
timeframe,
|
||||
matrixHistTotal,
|
||||
eqlQueryTotal,
|
||||
ruleType,
|
||||
setNoiseWarning,
|
||||
thresholdFieldExists,
|
||||
buckets.length,
|
||||
]);
|
||||
|
||||
const handlePreviewEqlQuery = useCallback(
|
||||
(to: string, from: string): void => {
|
||||
|
@ -263,8 +283,9 @@ export const PreviewQuery = ({
|
|||
options={timeframeOptions}
|
||||
value={timeframe}
|
||||
onChange={handleSelectPreviewTimeframe}
|
||||
aria-label={i18n.PREVIEW_SELECT_ARIA}
|
||||
aria-label={i18n.QUERY_PREVIEW_SELECT_ARIA}
|
||||
disabled={isDisabled}
|
||||
data-test-subj="queryPreviewTimeframeSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -276,7 +297,7 @@ export const PreviewQuery = ({
|
|||
onClick={handlePreviewClicked}
|
||||
data-test-subj="queryPreviewButton"
|
||||
>
|
||||
{i18n.PREVIEW_LABEL}
|
||||
{i18n.QUERY_PREVIEW_BUTTON}
|
||||
</PreviewButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -307,7 +328,6 @@ export const PreviewQuery = ({
|
|||
<PreviewEqlQueryHistogram
|
||||
to={toTime}
|
||||
from={fromTime}
|
||||
query={queryString}
|
||||
totalCount={eqlQueryTotal}
|
||||
data={eqlQueryData}
|
||||
inspect={eqlQueryInspect}
|
||||
|
|
|
@ -33,9 +33,9 @@ describe('queryPreviewReducer', () => {
|
|||
expect(update).toEqual(initialState);
|
||||
});
|
||||
|
||||
test('should reset showHistogram and warnings if queryBar undefined', () => {
|
||||
test('should reset showHistogram if queryBar undefined', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, showHistogram: true, warnings: ['uh oh'] },
|
||||
{ ...initialState, showHistogram: true },
|
||||
{
|
||||
type: 'setQueryInfo',
|
||||
queryBar: undefined,
|
||||
|
@ -44,11 +44,10 @@ describe('queryPreviewReducer', () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(update.warnings).toEqual([]);
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should reset showHistogram and warnings if queryBar defined', () => {
|
||||
test('should reset showHistogram if queryBar defined', () => {
|
||||
const update = reducer(
|
||||
{ ...initialState, showHistogram: true, warnings: ['uh oh'] },
|
||||
{
|
||||
|
@ -62,7 +61,6 @@ describe('queryPreviewReducer', () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(update.warnings).toEqual([]);
|
||||
expect(update.showHistogram).toBeFalsy();
|
||||
});
|
||||
|
||||
|
|
|
@ -82,13 +82,11 @@ export const queryPreviewReducer = () => (state: State, action: Action): State =
|
|||
filters,
|
||||
queryFilter,
|
||||
showHistogram: false,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
warnings: [],
|
||||
showHistogram: false,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -56,12 +56,12 @@ export const PreviewThresholdQueryHistogram = ({
|
|||
};
|
||||
}, [buckets]);
|
||||
|
||||
const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(200), []);
|
||||
const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(), []);
|
||||
|
||||
const subtitle = useMemo(
|
||||
(): string =>
|
||||
isLoading
|
||||
? i18n.PREVIEW_SUBTITLE_LOADING
|
||||
? i18n.QUERY_PREVIEW_SUBTITLE_LOADING
|
||||
: i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(totalCount),
|
||||
[isLoading, totalCount]
|
||||
);
|
||||
|
@ -73,7 +73,7 @@ export const PreviewThresholdQueryHistogram = ({
|
|||
barConfig={barConfig}
|
||||
title={i18n.QUERY_GRAPH_HITS_TITLE}
|
||||
subtitle={subtitle}
|
||||
disclaimer={i18n.PREVIEW_QUERY_DISCLAIMER}
|
||||
disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER}
|
||||
isLoading={isLoading}
|
||||
data-test-subj="thresholdQueryPreviewHistogram"
|
||||
/>
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const PREVIEW_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.stepDefineRule.previewQueryLabel',
|
||||
export const QUERY_PREVIEW_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.stepDefineRule.previewQueryButton',
|
||||
{
|
||||
defaultMessage: 'Preview results',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVIEW_SELECT_ARIA = i18n.translate(
|
||||
export const QUERY_PREVIEW_SELECT_ARIA = i18n.translate(
|
||||
'xpack.securitySolution.stepDefineRule.previewQueryAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Query preview timeframe select',
|
||||
|
@ -85,14 +85,14 @@ export const QUERY_PREVIEW_NO_HITS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PREVIEW_QUERY_ERROR = i18n.translate(
|
||||
export const QUERY_PREVIEW_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError',
|
||||
{
|
||||
defaultMessage: 'Error fetching preview',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVIEW_QUERY_DISCLAIMER = i18n.translate(
|
||||
export const QUERY_PREVIEW_DISCLAIMER = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer',
|
||||
{
|
||||
defaultMessage:
|
||||
|
@ -100,7 +100,7 @@ export const PREVIEW_QUERY_DISCLAIMER = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PREVIEW_QUERY_DISCLAIMER_EQL = i18n.translate(
|
||||
export const QUERY_PREVIEW_DISCLAIMER_EQL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql',
|
||||
{
|
||||
defaultMessage:
|
||||
|
@ -108,26 +108,24 @@ export const PREVIEW_QUERY_DISCLAIMER_EQL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PREVIEW_WARNING_CAP_HIT = (cap: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning',
|
||||
{
|
||||
values: { cap },
|
||||
defaultMessage:
|
||||
'Hit query cap size of {cap}. This query could produce more hits than the {cap} shown.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVIEW_WARNING_TIMESTAMP = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning',
|
||||
{
|
||||
defaultMessage: 'Unable to find "@timestamp" field on events.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVIEW_SUBTITLE_LOADING = i18n.translate(
|
||||
export const QUERY_PREVIEW_SUBTITLE_LOADING = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading',
|
||||
{
|
||||
defaultMessage: '...loading',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUERY_PREVIEW_EQL_SEQUENCE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewEqlSequenceTitle',
|
||||
{
|
||||
defaultMessage: 'No histogram available',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUERY_PREVIEW_EQL_SEQUENCE_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewEqlSequenceDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'No histogram is available at this time for EQL sequence queries. You can use the inspect in the top right corner to view query details.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
|
||||
import React, { FC, memo, useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
// Prefer importing entire lodash library, e.g. import { get } from "lodash"
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
|
@ -53,7 +53,7 @@ import {
|
|||
import { EqlQueryBar } from '../eql_query_bar';
|
||||
import { ThreatMatchInput } from '../threatmatch_input';
|
||||
import { useFetchIndex } from '../../../../common/containers/source';
|
||||
import { PreviewQuery } from '../query_preview';
|
||||
import { PreviewQuery, Threshold } from '../query_preview';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -210,6 +210,12 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
setOpenTimelineSearch(false);
|
||||
}, []);
|
||||
|
||||
const thresholdFormValue = useMemo((): Threshold | undefined => {
|
||||
return formThresholdValue != null && formThresholdField != null
|
||||
? { value: formThresholdValue, field: formThresholdField[0] }
|
||||
: undefined;
|
||||
}, [formThresholdField, formThresholdValue]);
|
||||
|
||||
const ThresholdInputChildren = useCallback(
|
||||
({ thresholdField, thresholdValue }) => (
|
||||
<ThresholdInput
|
||||
|
@ -403,11 +409,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
index={index}
|
||||
query={formQuery}
|
||||
isDisabled={queryBarQuery.trim() === '' || !isQueryBarValid || index.length === 0}
|
||||
threshold={
|
||||
formThresholdValue != null && formThresholdField != null
|
||||
? { value: formThresholdValue, field: formThresholdField[0] }
|
||||
: undefined
|
||||
}
|
||||
threshold={thresholdFormValue}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -17356,9 +17356,7 @@
|
|||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "クエリプレビュー",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "ノイズ警告:このルールではノイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "ヒットが見つかりませんでした。",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "クエリの上限サイズ{cap}に達しました。このクエリは表示されている{cap}を超えるヒットを生成できませんでした。",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} {buckets, plural, =1 {固有のヒット} other {固有のヒット}}",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "イベントで「@timestamp」フィールドが見つかりません",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} {hits, plural, =1 {ヒット} other {ヒット}}",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "クイッククエリプレビュー",
|
||||
|
@ -18356,7 +18354,6 @@
|
|||
"xpack.securitySolution.security.title": "セキュリティ",
|
||||
"xpack.securitySolution.source.destination.packetsLabel": "パケット",
|
||||
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "クエリプレビュータイムフレーム選択",
|
||||
"xpack.securitySolution.stepDefineRule.previewQueryLabel": "結果を表示",
|
||||
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "次の手段で接続を受け付けました。",
|
||||
"xpack.securitySolution.system.acceptedDescription": "以下を経由してユーザーを受け入れました。",
|
||||
"xpack.securitySolution.system.attemptedLoginDescription": "以下を経由してログインを試行しました:",
|
||||
|
|
|
@ -17374,9 +17374,7 @@
|
|||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "查询预览",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryNoHits": "找不到任何命中。",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphCapHitWarning": "命中查询上限大小为 {cap}。此查询生成的命中数可能大于显示的 {cap}。",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphThresholdWithFieldTitle": "{buckets} 个{buckets, plural, =1 {唯一命中} other {唯一命中}}",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTimestampWarning": "在事件中找不到“@timestamp”字段。",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewGraphTitle": "{hits} 个{hits, plural, =1 {命中} other {命中}}",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "快速查询预览",
|
||||
|
@ -18375,7 +18373,6 @@
|
|||
"xpack.securitySolution.security.title": "安全",
|
||||
"xpack.securitySolution.source.destination.packetsLabel": "pkts",
|
||||
"xpack.securitySolution.stepDefineRule.previewQueryAriaLabel": "查询预览时间范围选择",
|
||||
"xpack.securitySolution.stepDefineRule.previewQueryLabel": "预览结果",
|
||||
"xpack.securitySolution.system.acceptedAConnectionViaDescription": "已接受连接 - 通过",
|
||||
"xpack.securitySolution.system.acceptedDescription": "已接受该用户 - 通过",
|
||||
"xpack.securitySolution.system.attemptedLoginDescription": "已尝试登录 - 通过",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue