mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Associate timeline filter/query/dataprovider to top-n for alerts events * fix pinned view when opening details panel * fix top-n to bring the right raw/all indices * review + do not add filter/query/dataprovider on Correlation/Pinned tab for topN Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
parent
dbeb79015e
commit
e7d73b1a9f
10 changed files with 225 additions and 27 deletions
|
@ -134,7 +134,7 @@ const DraggableWrapperHoverContentComponent: React.FC<Props> = ({
|
|||
)
|
||||
? SourcererScopeName.detections
|
||||
: SourcererScopeName.default;
|
||||
const { browserFields, indexPattern, selectedPatterns } = useSourcererScope(activeScope);
|
||||
const { browserFields, indexPattern } = useSourcererScope(activeScope);
|
||||
const handleStartDragToTimeline = useCallback(() => {
|
||||
startDragToTimeline();
|
||||
if (closePopOver != null) {
|
||||
|
@ -365,7 +365,6 @@ const DraggableWrapperHoverContentComponent: React.FC<Props> = ({
|
|||
browserFields={browserFields}
|
||||
field={field}
|
||||
indexPattern={indexPattern}
|
||||
indexNames={selectedPatterns}
|
||||
onFilterAdded={onFilterAdded}
|
||||
timelineId={timelineId ?? undefined}
|
||||
toggleTopN={toggleTopN}
|
||||
|
|
|
@ -168,7 +168,6 @@ const store = createStore(
|
|||
let testProps = {
|
||||
browserFields: mockBrowserFields,
|
||||
field,
|
||||
indexNames: [],
|
||||
indexPattern: mockIndexPattern,
|
||||
timelineId: TimelineId.hostsPageExternalAlerts,
|
||||
toggleTopN: jest.fn(),
|
||||
|
|
|
@ -25,7 +25,7 @@ import { combineQueries } from '../../../timelines/components/timeline/helpers';
|
|||
|
||||
import { getOptions } from './helpers';
|
||||
import { TopN } from './top_n';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
|
||||
|
||||
const EMPTY_FILTERS: Filter[] = [];
|
||||
const EMPTY_QUERY: Query = { query: '', language: 'kuery' };
|
||||
|
@ -47,11 +47,16 @@ const makeMapStateToProps = () => {
|
|||
|
||||
return {
|
||||
activeTimelineEventType: activeTimeline.eventType,
|
||||
activeTimelineFilters,
|
||||
activeTimelineFilters:
|
||||
activeTimeline.activeTab === TimelineTabs.query ? activeTimelineFilters : EMPTY_FILTERS,
|
||||
activeTimelineFrom: activeTimelineInput.timerange.from,
|
||||
activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, TimelineId.active),
|
||||
activeTimelineKqlQueryExpression:
|
||||
activeTimeline.activeTab === TimelineTabs.query
|
||||
? getKqlQueryTimeline(state, TimelineId.active)
|
||||
: null,
|
||||
activeTimelineTo: activeTimelineInput.timerange.to,
|
||||
dataProviders: activeTimeline.dataProviders,
|
||||
dataProviders:
|
||||
activeTimeline.activeTab === TimelineTabs.query ? activeTimeline.dataProviders : [],
|
||||
globalQuery: getGlobalQuerySelector(state),
|
||||
globalFilters: getGlobalFiltersQuerySelector(state),
|
||||
kqlMode: activeTimeline.kqlMode,
|
||||
|
@ -72,7 +77,6 @@ interface OwnProps {
|
|||
browserFields: BrowserFields;
|
||||
field: string;
|
||||
indexPattern: IIndexPattern;
|
||||
indexNames: string[];
|
||||
timelineId?: string;
|
||||
toggleTopN: () => void;
|
||||
onFilterAdded?: () => void;
|
||||
|
@ -91,7 +95,6 @@ const StatefulTopNComponent: React.FC<Props> = ({
|
|||
dataProviders,
|
||||
field,
|
||||
indexPattern,
|
||||
indexNames,
|
||||
globalFilters = EMPTY_FILTERS,
|
||||
globalQuery = EMPTY_QUERY,
|
||||
kqlMode,
|
||||
|
@ -154,7 +157,6 @@ const StatefulTopNComponent: React.FC<Props> = ({
|
|||
filters={timelineId === TimelineId.active ? EMPTY_FILTERS : globalFilters}
|
||||
from={timelineId === TimelineId.active ? activeTimelineFrom : from}
|
||||
indexPattern={indexPattern}
|
||||
indexNames={indexNames}
|
||||
options={options}
|
||||
query={timelineId === TimelineId.active ? EMPTY_QUERY : globalQuery}
|
||||
setAbsoluteRangeDatePickerTarget={timelineId === TimelineId.active ? 'timeline' : 'global'}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { State } from '../../store';
|
||||
import { sourcererSelectors } from '../../store/selectors';
|
||||
|
||||
export interface IndicesSelector {
|
||||
all: string[];
|
||||
raw: string[];
|
||||
}
|
||||
|
||||
export const getIndicesSelector = () => {
|
||||
const getkibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector();
|
||||
const getConfigIndexPatternsSelector = sourcererSelectors.configIndexPatternsSelector();
|
||||
const getSignalIndexNameSelector = sourcererSelectors.signalIndexNameSelector();
|
||||
|
||||
const mapStateToProps = (state: State): IndicesSelector => {
|
||||
const rawIndices = new Set(getConfigIndexPatternsSelector(state));
|
||||
const kibanaIndexPatterns = getkibanaIndexPatternsSelector(state);
|
||||
const alertIndexName = getSignalIndexNameSelector(state);
|
||||
kibanaIndexPatterns.forEach(({ title }) => {
|
||||
if (title !== alertIndexName) {
|
||||
rawIndices.add(title);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
all: alertIndexName != null ? [...rawIndices, alertIndexName] : [...rawIndices],
|
||||
raw: [...rawIndices],
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
|
@ -101,7 +101,6 @@ describe('TopN', () => {
|
|||
field,
|
||||
filters: [],
|
||||
from: '2020-04-14T00:31:47.695Z',
|
||||
indexNames: [],
|
||||
indexPattern: mockIndexPattern,
|
||||
options: defaultOptions,
|
||||
query,
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonIcon, EuiSuperSelect } from '@elastic/eui';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { GlobalTimeArgs } from '../../containers/use_global_time';
|
||||
|
@ -18,6 +20,8 @@ import { TimelineEventsType } from '../../../../common/types/timeline';
|
|||
|
||||
import { TopNOption } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import { getIndicesSelector, IndicesSelector } from './selectors';
|
||||
import { State } from '../../store';
|
||||
|
||||
const TopNContainer = styled.div`
|
||||
width: 600px;
|
||||
|
@ -49,7 +53,6 @@ export interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery
|
|||
field: string;
|
||||
filters: Filter[];
|
||||
indexPattern: IIndexPattern;
|
||||
indexNames: string[];
|
||||
options: TopNOption[];
|
||||
query: Query;
|
||||
setAbsoluteRangeDatePickerTarget: InputsModelId;
|
||||
|
@ -67,7 +70,6 @@ const TopNComponent: React.FC<Props> = ({
|
|||
field,
|
||||
from,
|
||||
indexPattern,
|
||||
indexNames,
|
||||
options,
|
||||
query,
|
||||
setAbsoluteRangeDatePickerTarget,
|
||||
|
@ -80,6 +82,11 @@ const TopNComponent: React.FC<Props> = ({
|
|||
const onViewSelected = useCallback((value: string) => setView(value as TimelineEventsType), [
|
||||
setView,
|
||||
]);
|
||||
const indicesSelector = useMemo(getIndicesSelector, []);
|
||||
const { all: allIndices, raw: rawIndices } = useSelector<State, IndicesSelector>(
|
||||
(state) => indicesSelector(state),
|
||||
deepEqual
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setView(defaultView);
|
||||
|
@ -116,7 +123,7 @@ const TopNComponent: React.FC<Props> = ({
|
|||
from={from}
|
||||
headerChildren={headerChildren}
|
||||
indexPattern={indexPattern}
|
||||
indexNames={indexNames}
|
||||
indexNames={view === 'raw' ? rawIndices : allIndices}
|
||||
onlyField={field}
|
||||
query={query}
|
||||
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
|
||||
|
@ -127,6 +134,7 @@ const TopNComponent: React.FC<Props> = ({
|
|||
/>
|
||||
) : (
|
||||
<SignalsByCategory
|
||||
combinedQueries={combinedQueries}
|
||||
filters={filters}
|
||||
from={from}
|
||||
headerChildren={headerChildren}
|
||||
|
|
|
@ -12,7 +12,8 @@ import { shallow, mount } from 'enzyme';
|
|||
import '../../../common/mock/match_media';
|
||||
import { esQuery } from '../../../../../../../src/plugins/data/public';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { AlertsHistogramPanel } from './index';
|
||||
import { AlertsHistogramPanel, buildCombinedQueries, parseCombinedQueries } from './index';
|
||||
import * as helpers from './helpers';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const originalModule = jest.requireActual('react-router-dom');
|
||||
|
@ -104,4 +105,125 @@ describe('AlertsHistogramPanel', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CombinedQueries', () => {
|
||||
jest.mock('./helpers');
|
||||
const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery');
|
||||
beforeEach(() => {
|
||||
mockGetAlertsHistogramQuery.mockReset();
|
||||
});
|
||||
|
||||
it('combinedQueries props is valid, alerts query include combinedQueries', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
query: { query: 'host.name: "', language: 'kql' },
|
||||
combinedQueries:
|
||||
'{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}',
|
||||
};
|
||||
mount(
|
||||
<TestProviders>
|
||||
<AlertsHistogramPanel {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockGetAlertsHistogramQuery.mock.calls[0]).toEqual([
|
||||
'signal.rule.name',
|
||||
'2020-07-07T08:20:18.966Z',
|
||||
'2020-07-08T08:20:18.966Z',
|
||||
[
|
||||
{
|
||||
bool: {
|
||||
filter: [{ match_all: {} }, { exists: { field: 'process.name' } }],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseCombinedQueries', () => {
|
||||
it('return empty object when variables is undefined', async () => {
|
||||
expect(parseCombinedQueries(undefined)).toEqual({});
|
||||
});
|
||||
|
||||
it('return empty object when variables is empty string', async () => {
|
||||
expect(parseCombinedQueries('')).toEqual({});
|
||||
});
|
||||
|
||||
it('return empty object when variables is NOT a valid stringify json object', async () => {
|
||||
expect(parseCombinedQueries('hello world')).toEqual({});
|
||||
});
|
||||
|
||||
it('return a valid json object when variables is a valid json stringify', async () => {
|
||||
expect(
|
||||
parseCombinedQueries(
|
||||
'{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}'
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "process.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
"must": Array [],
|
||||
"must_not": Array [],
|
||||
"should": Array [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCombinedQueries', () => {
|
||||
it('return empty array when variables is undefined', async () => {
|
||||
expect(buildCombinedQueries(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('return empty array when variables is empty string', async () => {
|
||||
expect(buildCombinedQueries('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('return array with empty object when variables is NOT a valid stringify json object', async () => {
|
||||
expect(buildCombinedQueries('hello world')).toEqual([{}]);
|
||||
});
|
||||
|
||||
it('return a valid json object when variables is a valid json stringify', async () => {
|
||||
expect(
|
||||
buildCombinedQueries(
|
||||
'{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}'
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "process.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
"must": Array [],
|
||||
"must_not": Array [],
|
||||
"should": Array [],
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,6 +58,7 @@ const ViewAlertsFlexItem = styled(EuiFlexItem)`
|
|||
interface AlertsHistogramPanelProps
|
||||
extends Pick<GlobalTimeArgs, 'from' | 'to' | 'setQuery' | 'deleteQuery'> {
|
||||
chartHeight?: number;
|
||||
combinedQueries?: string;
|
||||
defaultStackByOption?: AlertsHistogramOption;
|
||||
filters?: Filter[];
|
||||
headerChildren?: React.ReactNode;
|
||||
|
@ -86,9 +87,26 @@ const DEFAULT_STACK_BY = 'signal.rule.name';
|
|||
const getDefaultStackByOption = (): AlertsHistogramOption =>
|
||||
alertsHistogramOptions.find(({ text }) => text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0];
|
||||
|
||||
export const parseCombinedQueries = (query?: string) => {
|
||||
try {
|
||||
return query != null && !isEmpty(query) ? JSON.parse(query) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const buildCombinedQueries = (query?: string) => {
|
||||
try {
|
||||
return isEmpty(query) ? [] : [parseCombinedQueries(query)];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
||||
({
|
||||
chartHeight,
|
||||
combinedQueries,
|
||||
defaultStackByOption = getDefaultStackByOption(),
|
||||
deleteQuery,
|
||||
filters,
|
||||
|
@ -124,7 +142,12 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
request,
|
||||
refetch,
|
||||
} = useQueryAlerts<{}, AlertsAggregation>(
|
||||
getAlertsHistogramQuery(selectedStackByOption.value, from, to, []),
|
||||
getAlertsHistogramQuery(
|
||||
selectedStackByOption.value,
|
||||
from,
|
||||
to,
|
||||
buildCombinedQueries(combinedQueries)
|
||||
),
|
||||
signalIndexName
|
||||
);
|
||||
const kibana = useKibana();
|
||||
|
@ -223,15 +246,20 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const converted = esQuery.buildEsQuery(
|
||||
undefined,
|
||||
query != null ? [query] : [],
|
||||
filters?.filter((f) => f.meta.disabled === false) ?? [],
|
||||
{
|
||||
...esQuery.getEsQueryConfig(kibana.services.uiSettings),
|
||||
dateFormatTZ: undefined,
|
||||
}
|
||||
);
|
||||
let converted = null;
|
||||
if (combinedQueries != null) {
|
||||
converted = parseCombinedQueries(combinedQueries);
|
||||
} else {
|
||||
converted = esQuery.buildEsQuery(
|
||||
undefined,
|
||||
query != null ? [query] : [],
|
||||
filters?.filter((f) => f.meta.disabled === false) ?? [],
|
||||
{
|
||||
...esQuery.getEsQueryConfig(kibana.services.uiSettings),
|
||||
dateFormatTZ: undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setAlertsQuery(
|
||||
getAlertsHistogramQuery(
|
||||
|
@ -245,7 +273,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption.value, from, to, []));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedStackByOption.value, from, to, query, filters]);
|
||||
}, [selectedStackByOption.value, from, to, query, filters, combinedQueries]);
|
||||
|
||||
const linkButton = useMemo(() => {
|
||||
if (showLinkToAlerts) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import { UpdateDateRange } from '../../../common/components/charts/common';
|
|||
import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
|
||||
|
||||
interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'setQuery'> {
|
||||
combinedQueries?: string;
|
||||
filters?: Filter[];
|
||||
headerChildren?: React.ReactNode;
|
||||
/** Override all defaults, and only display this field */
|
||||
|
@ -29,6 +30,7 @@ interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'se
|
|||
}
|
||||
|
||||
const SignalsByCategoryComponent: React.FC<Props> = ({
|
||||
combinedQueries,
|
||||
deleteQuery,
|
||||
filters,
|
||||
from,
|
||||
|
@ -61,6 +63,7 @@ const SignalsByCategoryComponent: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<AlertsHistogramPanel
|
||||
combinedQueries={combinedQueries}
|
||||
deleteQuery={deleteQuery}
|
||||
filters={filters}
|
||||
from={from}
|
||||
|
|
|
@ -198,7 +198,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<FullWidthFlexGroup data-test-subj={`${TimelineTabs.pinned}-tab`} direction="column">
|
||||
<FullWidthFlexGroup data-test-subj={`${TimelineTabs.pinned}-tab`}>
|
||||
{timelineFullScreen && setTimelineFullScreen != null && (
|
||||
<ExitFullScreenFlexItem grow={false}>
|
||||
<ExitFullScreen fullScreen={timelineFullScreen} setFullScreen={setTimelineFullScreen} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue