[Discover][Alerting] Implement editing of dataView, query & filters (#131688)

* [Discover] introduce params editing using unified search

* [Discover] fix unit tests

* [Discover] fix functional tests

* [Discover] fix unit tests

* [Discover] return test subject name

* [Discover] fix alert functional test

* Update x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx

Co-authored-by: Julia Rechkunova <julia.rechkunova@gmail.com>

* Update x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx

Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>

* [Discover] hide filter panel options

* [Discover] improve functional test

* [Discover] apply suggestions

* [Discover] change data view selector

* [Discover] fix tests

* [Discover] apply suggestions, fix lang mode toggler

* [Discover] mote interface to types file, clean up diff

* [Discover] fix saved query issue

* Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts

Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>

* [Discover] remove zIndex

* [Discover] omit null searchType from esQuery completely, add isEsQueryAlert check for useSavedObjectReferences hook

* [Discover] set searchType to esQuery when needed

* [Discover] fix unit tests

* Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts

Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>

* Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts

Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>

Co-authored-by: Julia Rechkunova <julia.rechkunova@gmail.com>
Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>
This commit is contained in:
Dmitry Tomashevich 2022-05-20 17:09:20 +03:00 committed by GitHub
parent d34408876a
commit bc31053dc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 800 additions and 416 deletions

View file

@ -14,7 +14,8 @@
"triggersActionsUi",
"kibanaReact",
"savedObjects",
"data"
"data",
"kibanaUtils"
],
"configPath": ["xpack", "stack_alerts"],
"requiredBundles": ["esUiShared"],

View file

@ -0,0 +1,75 @@
/*
* 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 React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { DataViewSelectPopover } from './data_view_select_popover';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { act } from 'react-dom/test-utils';
const props = {
onSelectDataView: () => {},
initialDataViewTitle: 'kibana_sample_data_logs',
initialDataViewId: 'mock-data-logs-id',
};
const dataViewOptions = [
{
id: 'mock-data-logs-id',
namespaces: ['default'],
title: 'kibana_sample_data_logs',
},
{
id: 'mock-flyghts-id',
namespaces: ['default'],
title: 'kibana_sample_data_flights',
},
{
id: 'mock-ecommerce-id',
namespaces: ['default'],
title: 'kibana_sample_data_ecommerce',
typeMeta: {},
},
{
id: 'mock-test-id',
namespaces: ['default'],
title: 'test',
typeMeta: {},
},
];
const mount = () => {
const dataViewsMock = dataViewPluginMocks.createStartContract();
dataViewsMock.getIdsWithTitle.mockImplementation(() => Promise.resolve(dataViewOptions));
return {
wrapper: mountWithIntl(
<KibanaContextProvider services={{ data: { dataViews: dataViewsMock } }}>
<DataViewSelectPopover {...props} />
</KibanaContextProvider>
),
dataViewsMock,
};
};
describe('DataViewSelectPopover', () => {
test('renders properly', async () => {
const { wrapper, dataViewsMock } = mount();
await act(async () => {
await nextTick();
wrapper.update();
});
expect(dataViewsMock.getIdsWithTitle).toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="selectDataViewExpression"]').exists()).toBeTruthy();
const getIdsWithTitleResult = await dataViewsMock.getIdsWithTitle.mock.results[0].value;
expect(getIdsWithTitleResult).toBe(dataViewOptions);
});
});

View file

@ -0,0 +1,120 @@
/*
* 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 React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonIcon,
EuiExpression,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPopover,
EuiPopoverTitle,
} from '@elastic/eui';
import { DataViewsList } from '@kbn/unified-search-plugin/public';
import { DataViewListItem } from '@kbn/data-views-plugin/public';
import { useTriggersAndActionsUiDeps } from '../es_query/util';
interface DataViewSelectPopoverProps {
onSelectDataView: (newDataViewId: string) => void;
initialDataViewTitle: string;
initialDataViewId?: string;
}
export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopoverProps> = ({
onSelectDataView,
initialDataViewTitle,
initialDataViewId,
}) => {
const { data } = useTriggersAndActionsUiDeps();
const [dataViewItems, setDataViewsItems] = useState<DataViewListItem[]>();
const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false);
const [selectedDataViewId, setSelectedDataViewId] = useState(initialDataViewId);
const [selectedTitle, setSelectedTitle] = useState<string>(initialDataViewTitle);
useEffect(() => {
const initDataViews = async () => {
const fetchedDataViewItems = await data.dataViews.getIdsWithTitle();
setDataViewsItems(fetchedDataViewItems);
};
initDataViews();
}, [data.dataViews]);
const closeDataViewPopover = useCallback(() => setDataViewPopoverOpen(false), []);
if (!dataViewItems) {
return null;
}
return (
<EuiPopover
id="dataViewPopover"
button={
<EuiExpression
display="columns"
data-test-subj="selectDataViewExpression"
description={i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewLabel', {
defaultMessage: 'data view',
})}
value={selectedTitle}
isActive={dataViewPopoverOpen}
onClick={() => {
setDataViewPopoverOpen(true);
}}
isInvalid={!selectedTitle}
/>
}
isOpen={dataViewPopoverOpen}
closePopover={closeDataViewPopover}
ownFocus
anchorPosition="downLeft"
display="block"
>
<div style={{ width: '450px' }} data-test-subj="chooseDataViewPopoverContent">
<EuiPopoverTitle>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>
{i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewPopoverTitle', {
defaultMessage: 'Data view',
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="closeDataViewPopover"
iconType="cross"
color="danger"
aria-label={i18n.translate(
'xpack.stackAlerts.components.ui.alertParams.closeDataViewPopoverLabel',
{ defaultMessage: 'Close' }
)}
onClick={closeDataViewPopover}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>
<EuiFormRow id="indexSelectSearchBox" fullWidth>
<DataViewsList
dataViewsList={dataViewItems}
onChangeDataView={(newId) => {
setSelectedDataViewId(newId);
const newTitle = dataViewItems?.find(({ id }) => id === newId)?.title;
if (newTitle) {
setSelectedTitle(newTitle);
}
onSelectDataView(newId);
closeDataViewPopover();
}}
currentDataViewId={selectedDataViewId}
/>
</EuiFormRow>
</div>
</EuiPopover>
);
};

View file

@ -6,6 +6,7 @@
*/
import { COMPARATORS } from '@kbn/triggers-actions-ui-plugin/public';
import { ErrorKey } from './types';
export const DEFAULT_VALUES = {
THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN,
@ -19,3 +20,17 @@ export const DEFAULT_VALUES = {
TIME_WINDOW_UNIT: 'm',
THRESHOLD: [1000],
};
export const EXPRESSION_ERRORS = {
index: new Array<string>(),
size: new Array<string>(),
timeField: new Array<string>(),
threshold0: new Array<string>(),
threshold1: new Array<string>(),
esQuery: new Array<string>(),
thresholdComparator: new Array<string>(),
timeWindowSize: new Array<string>(),
searchConfiguration: new Array<string>(),
};
export const EXPRESSION_ERROR_KEYS = Object.keys(EXPRESSION_ERRORS) as ErrorKey[];

View file

@ -83,6 +83,7 @@ export const EsQueryExpression = ({
thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
size: size ?? DEFAULT_VALUES.SIZE,
esQuery: esQuery ?? DEFAULT_VALUES.QUERY,
searchType: 'esQuery',
});
const setParam = useCallback(

View file

@ -5,29 +5,33 @@
* 2.0.
*/
import React from 'react';
import React, { memo, PropsWithChildren } from 'react';
import { i18n } from '@kbn/i18n';
import deepEqual from 'fast-deep-equal';
import 'brace/theme/github';
import { EuiSpacer, EuiCallOut } from '@elastic/eui';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { EsQueryAlertParams } from '../types';
import { SearchSourceExpression } from './search_source_expression';
import { ErrorKey, EsQueryAlertParams } from '../types';
import { SearchSourceExpression, SearchSourceExpressionProps } from './search_source_expression';
import { EsQueryExpression } from './es_query_expression';
import { isSearchSourceAlert } from '../util';
import { EXPRESSION_ERROR_KEYS } from '../constants';
const expressionFieldsWithValidation = [
'index',
'size',
'timeField',
'threshold0',
'threshold1',
'timeWindowSize',
'searchType',
'esQuery',
'searchConfiguration',
];
function areSearchSourceExpressionPropsEqual(
prevProps: Readonly<PropsWithChildren<SearchSourceExpressionProps>>,
nextProps: Readonly<PropsWithChildren<SearchSourceExpressionProps>>
) {
const areErrorsEqual = deepEqual(prevProps.errors, nextProps.errors);
const areRuleParamsEqual = deepEqual(prevProps.ruleParams, nextProps.ruleParams);
return areErrorsEqual && areRuleParamsEqual;
}
const SearchSourceExpressionMemoized = memo<SearchSourceExpressionProps>(
SearchSourceExpression,
areSearchSourceExpressionPropsEqual
);
export const EsQueryAlertTypeExpression: React.FunctionComponent<
RuleTypeParamsExpressionProps<EsQueryAlertParams>
@ -35,11 +39,11 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
const { ruleParams, errors } = props;
const isSearchSource = isSearchSourceAlert(ruleParams);
const hasExpressionErrors = !!Object.keys(errors).find((errorKey) => {
const hasExpressionErrors = Object.keys(errors).some((errorKey) => {
return (
expressionFieldsWithValidation.includes(errorKey) &&
EXPRESSION_ERROR_KEYS.includes(errorKey as ErrorKey) &&
errors[errorKey].length >= 1 &&
ruleParams[errorKey as keyof EsQueryAlertParams] !== undefined
ruleParams[errorKey] !== undefined
);
});
@ -54,14 +58,13 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
<>
{hasExpressionErrors && (
<>
<EuiSpacer />
<EuiCallOut color="danger" size="s" title={expressionErrorMessage} />
<EuiSpacer />
</>
)}
{isSearchSource ? (
<SearchSourceExpression {...props} ruleParams={ruleParams} />
<SearchSourceExpressionMemoized {...props} ruleParams={ruleParams} />
) : (
<EsQueryExpression {...props} ruleParams={ruleParams} />
)}

View file

@ -1,66 +0,0 @@
/*
* 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 React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { getDisplayValueFromFilter } from '@kbn/data-plugin/public';
import { Filter } from '@kbn/data-plugin/common';
import { DataView } from '@kbn/data-views-plugin/public';
import { FilterItem } from '@kbn/unified-search-plugin/public';
const FilterItemComponent = injectI18n(FilterItem);
interface ReadOnlyFilterItemsProps {
filters: Filter[];
indexPatterns: DataView[];
}
const noOp = () => {};
export const ReadOnlyFilterItems = ({ filters, indexPatterns }: ReadOnlyFilterItemsProps) => {
const { uiSettings } = useKibana().services;
const filterList = filters.map((filter, index) => {
const filterValue = getDisplayValueFromFilter(filter, indexPatterns);
return (
<EuiFlexItem
grow={false}
css={css`
max-width: 100%;
`}
>
<FilterItemComponent
key={`${filter.meta.key}${filterValue}`}
id={`${index}`}
filter={filter}
onUpdate={noOp}
onRemove={noOp}
indexPatterns={indexPatterns}
uiSettings={uiSettings!}
readonly
/>
</EuiFlexItem>
);
});
return (
<EuiFlexGroup
className="globalFilterBar"
wrap={true}
responsive={false}
gutterSize="xs"
alignItems="center"
tabIndex={-1}
>
{filterList}
</EuiFlexGroup>
);
};

View file

@ -10,18 +10,12 @@ import React from 'react';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { DataPublicPluginStart, ISearchStart } from '@kbn/data-plugin/public';
import { EsQueryAlertParams, SearchType } from '../types';
import { SearchSourceExpression } from './search_source_expression';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { act } from 'react-dom/test-utils';
import { EuiCallOut, EuiLoadingSpinner } from '@elastic/eui';
import { ReactWrapper } from 'enzyme';
const dataMock = dataPluginMock.createStartContract() as DataPublicPluginStart & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
search: ISearchStart & { searchSource: { create: jest.MockedFunction<any> } };
};
import { EuiLoadingSpinner } from '@elastic/eui';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
const dataViewPluginMock = dataViewPluginMocks.createStartContract();
const chartsStartMock = chartPluginMock.createStartContract();
@ -40,6 +34,18 @@ const defaultSearchSourceExpressionParams: EsQueryAlertParams<SearchType.searchS
};
const searchSourceMock = {
id: 'data_source6',
fields: {
query: {
query: '',
language: 'kuery',
},
filter: [],
index: {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
title: 'kibana_sample_data_logs',
},
},
getField: (name: string) => {
if (name === 'filter') {
return [];
@ -48,7 +54,33 @@ const searchSourceMock = {
},
};
const setup = async (alertParams: EsQueryAlertParams<SearchType.searchSource>) => {
const savedQueryMock = {
id: 'test-id',
attributes: {
title: 'test-filter-set',
description: '',
query: {
query: 'category.keyword : "Men\'s Shoes" ',
language: 'kuery',
},
filters: [],
},
};
jest.mock('./search_source_expression_form', () => ({
SearchSourceExpressionForm: () => <div>search source expression form mock</div>,
}));
const dataMock = dataPluginMock.createStartContract();
(dataMock.search.searchSource.create as jest.Mock).mockImplementation(() =>
Promise.resolve(searchSourceMock)
);
(dataMock.dataViews.getIdsWithTitle as jest.Mock).mockImplementation(() => Promise.resolve([]));
(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() =>
Promise.resolve(savedQueryMock)
);
const setup = (alertParams: EsQueryAlertParams<SearchType.searchSource>) => {
const errors = {
size: [],
timeField: [],
@ -57,67 +89,58 @@ const setup = async (alertParams: EsQueryAlertParams<SearchType.searchSource>) =
};
const wrapper = mountWithIntl(
<SearchSourceExpression
ruleInterval="1m"
ruleThrottle="1m"
alertNotifyWhen="onThrottleInterval"
ruleParams={alertParams}
setRuleParams={() => {}}
setRuleProperty={() => {}}
errors={errors}
unifiedSearch={unifiedSearchMock}
data={dataMock}
dataViews={dataViewPluginMock}
defaultActionGroupId=""
actionGroups={[]}
charts={chartsStartMock}
/>
<KibanaContextProvider services={{ data: dataMock }}>
<SearchSourceExpression
ruleInterval="1m"
ruleThrottle="1m"
alertNotifyWhen="onThrottleInterval"
ruleParams={alertParams}
setRuleParams={() => {}}
setRuleProperty={() => {}}
errors={errors}
unifiedSearch={unifiedSearchMock}
data={dataMock}
dataViews={dataViewPluginMock}
defaultActionGroupId=""
actionGroups={[]}
charts={chartsStartMock}
/>
</KibanaContextProvider>
);
return wrapper;
};
const rerender = async (wrapper: ReactWrapper) => {
const update = async () =>
await act(async () => {
await nextTick();
wrapper.update();
});
await update();
};
describe('SearchSourceAlertTypeExpression', () => {
test('should render loading prompt', async () => {
dataMock.search.searchSource.create.mockImplementation(() =>
Promise.resolve(() => searchSourceMock)
);
const wrapper = await setup(defaultSearchSourceExpressionParams);
test('should render correctly', async () => {
let wrapper = setup(defaultSearchSourceExpressionParams).children();
expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy();
expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy();
await act(async () => {
await nextTick();
});
wrapper = await wrapper.update();
expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy();
expect(wrapper.text().includes('search source expression form mock')).toBeTruthy();
});
test('should render error prompt', async () => {
dataMock.search.searchSource.create.mockImplementation(() =>
Promise.reject(() => 'test error')
(dataMock.search.searchSource.create as jest.Mock).mockImplementationOnce(() =>
Promise.reject(new Error('Cant find searchSource'))
);
let wrapper = setup(defaultSearchSourceExpressionParams).children();
const wrapper = await setup(defaultSearchSourceExpressionParams);
await rerender(wrapper);
expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy();
expect(wrapper.text().includes('Cant find searchSource')).toBeFalsy();
expect(wrapper.find(EuiCallOut).exists()).toBeTruthy();
});
await act(async () => {
await nextTick();
});
wrapper = await wrapper.update();
test('should render SearchSourceAlertTypeExpression with expected components', async () => {
dataMock.search.searchSource.create.mockImplementation(() =>
Promise.resolve(() => searchSourceMock)
);
const wrapper = await setup(defaultSearchSourceExpressionParams);
await rerender(wrapper);
expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy();
expect(wrapper.text().includes('Cant find searchSource')).toBeTruthy();
});
});

View file

@ -5,36 +5,27 @@
* 2.0.
*/
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import './search_source_expression.scss';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiSpacer,
EuiTitle,
EuiExpression,
EuiLoadingSpinner,
EuiEmptyPrompt,
EuiCallOut,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Filter, ISearchSource } from '@kbn/data-plugin/common';
import {
ForLastExpression,
RuleTypeParamsExpressionProps,
ThresholdExpression,
ValueExpression,
} from '@kbn/triggers-actions-ui-plugin/public';
import { EuiSpacer, EuiLoadingSpinner, EuiEmptyPrompt, EuiCallOut } from '@elastic/eui';
import { ISearchSource } from '@kbn/data-plugin/common';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { SavedQuery } from '@kbn/data-plugin/public';
import { EsQueryAlertParams, SearchType } from '../types';
import { useTriggersAndActionsUiDeps } from '../util';
import { SearchSourceExpressionForm } from './search_source_expression_form';
import { DEFAULT_VALUES } from '../constants';
import { ReadOnlyFilterItems } from './read_only_filter_items';
export type SearchSourceExpressionProps = RuleTypeParamsExpressionProps<
EsQueryAlertParams<SearchType.searchSource>
>;
export const SearchSourceExpression = ({
ruleParams,
errors,
setRuleParams,
setRuleProperty,
data,
errors,
}: RuleTypeParamsExpressionProps<EsQueryAlertParams<SearchType.searchSource>>) => {
}: SearchSourceExpressionProps) => {
const {
searchConfiguration,
thresholdComparator,
@ -43,48 +34,43 @@ export const SearchSourceExpression = ({
timeWindowUnit,
size,
} = ruleParams;
const [usedSearchSource, setUsedSearchSource] = useState<ISearchSource | undefined>();
const [paramsError, setParamsError] = useState<Error | undefined>();
const { data } = useTriggersAndActionsUiDeps();
const [currentAlertParams, setCurrentAlertParams] = useState<
EsQueryAlertParams<SearchType.searchSource>
>({
searchConfiguration,
searchType: SearchType.searchSource,
timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
size: size ?? DEFAULT_VALUES.SIZE,
});
const [searchSource, setSearchSource] = useState<ISearchSource>();
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
const [paramsError, setParamsError] = useState<Error>();
const setParam = useCallback(
(paramField: string, paramValue: unknown) => {
setCurrentAlertParams((currentParams) => ({
...currentParams,
[paramField]: paramValue,
}));
setRuleParams(paramField, paramValue);
},
(paramField: string, paramValue: unknown) => setRuleParams(paramField, paramValue),
[setRuleParams]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setRuleProperty('params', currentAlertParams), []);
useEffect(() => {
setRuleProperty('params', {
searchConfiguration,
searchType: SearchType.searchSource,
timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
size: size ?? DEFAULT_VALUES.SIZE,
});
const initSearchSource = () =>
data.search.searchSource
.create(searchConfiguration)
.then((fetchedSearchSource) => setSearchSource(fetchedSearchSource))
.catch(setParamsError);
initSearchSource();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.search.searchSource, data.dataViews]);
useEffect(() => {
async function initSearchSource() {
try {
const loadedSearchSource = await data.search.searchSource.create(searchConfiguration);
setUsedSearchSource(loadedSearchSource);
} catch (error) {
setParamsError(error);
}
if (ruleParams.savedQueryId) {
data.query.savedQueries.getSavedQuery(ruleParams.savedQueryId).then(setSavedQuery);
}
if (searchConfiguration) {
initSearchSource();
}
}, [data.search.searchSource, searchConfiguration]);
}, [data.query.savedQueries, ruleParams.savedQueryId]);
if (paramsError) {
return (
@ -97,124 +83,17 @@ export const SearchSourceExpression = ({
);
}
if (!usedSearchSource) {
if (!searchSource) {
return <EuiEmptyPrompt title={<EuiLoadingSpinner size="xl" />} />;
}
const dataView = usedSearchSource.getField('index')!;
const query = usedSearchSource.getField('query')!;
const filters = (usedSearchSource.getField('filter') as Filter[]).filter(
({ meta }) => !meta.disabled
);
const dataViews = [dataView];
return (
<Fragment>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchThreshold.ui.conditionPrompt"
defaultMessage="When the number of documents match"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.stackAlerts.searchThreshold.ui.notEditable"
defaultMessage="The data view, query, and filter are initialized in Discover and cannot be edited."
/>
}
iconType="iInCircle"
/>
<EuiSpacer size="s" />
<EuiExpression
className="dscExpressionParam"
description={'Data view'}
value={dataView.title}
display="columns"
/>
{query.query !== '' && (
<EuiExpression
className="dscExpressionParam"
description={'Query'}
value={query.query}
display="columns"
/>
)}
{filters.length > 0 && (
<EuiExpression
className="dscExpressionParam searchSourceAlertFilters"
title={'sas'}
description={'Filter'}
value={<ReadOnlyFilterItems filters={filters} indexPatterns={dataViews} />}
display="columns"
/>
)}
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchSource.ui.conditionPrompt"
defaultMessage="When the number of matches"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ThresholdExpression
data-test-subj="thresholdExpression"
thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR}
threshold={threshold ?? DEFAULT_VALUES.THRESHOLD}
errors={errors}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedThreshold={(selectedThresholds) =>
setParam('threshold', selectedThresholds)
}
onChangeSelectedThresholdComparator={(selectedThresholdComparator) =>
setParam('thresholdComparator', selectedThresholdComparator)
}
/>
<ForLastExpression
data-test-subj="forLastExpression"
popupPosition={'upLeft'}
timeWindowSize={timeWindowSize}
timeWindowUnit={timeWindowUnit}
display="fullWidth"
errors={errors}
onChangeWindowSize={(selectedWindowSize: number | undefined) =>
setParam('timeWindowSize', selectedWindowSize)
}
onChangeWindowUnit={(selectedWindowUnit: string) =>
setParam('timeWindowUnit', selectedWindowUnit)
}
/>
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchSource.ui.selectSizePrompt"
defaultMessage="Select a size"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ValueExpression
description={i18n.translate('xpack.stackAlerts.searchSource.ui.sizeExpression', {
defaultMessage: 'Size',
})}
data-test-subj="sizeValueExpression"
value={size}
errors={errors.size}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedValue={(updatedValue) => {
setParam('size', updatedValue);
}}
/>
<EuiSpacer size="s" />
</Fragment>
<SearchSourceExpressionForm
searchSource={searchSource}
errors={errors}
ruleParams={ruleParams}
initialSavedQuery={savedQuery}
setParam={setParam}
/>
);
};

View file

@ -0,0 +1,269 @@
/*
* 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 React, { Fragment, useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Filter, DataView, Query, ISearchSource } from '@kbn/data-plugin/common';
import {
ForLastExpression,
IErrorObject,
ThresholdExpression,
ValueExpression,
} from '@kbn/triggers-actions-ui-plugin/public';
import { SearchBar } from '@kbn/unified-search-plugin/public';
import { mapAndFlattenFilters, SavedQuery, TimeHistory } from '@kbn/data-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { EsQueryAlertParams, SearchType } from '../types';
import { DEFAULT_VALUES } from '../constants';
import { DataViewSelectPopover } from '../../components/data_view_select_popover';
import { useTriggersAndActionsUiDeps } from '../util';
interface LocalState {
index: DataView;
filter: Filter[];
query: Query;
threshold: number[];
timeWindowSize: number;
size: number;
}
interface LocalStateAction {
type: SearchSourceParamsAction['type'] | ('threshold' | 'timeWindowSize' | 'size');
payload: SearchSourceParamsAction['payload'] | (number[] | number);
}
type LocalStateReducer = (prevState: LocalState, action: LocalStateAction) => LocalState;
interface SearchSourceParamsAction {
type: 'index' | 'filter' | 'query';
payload: DataView | Filter[] | Query;
}
interface SearchSourceExpressionFormProps {
searchSource: ISearchSource;
ruleParams: EsQueryAlertParams<SearchType.searchSource>;
errors: IErrorObject;
initialSavedQuery?: SavedQuery;
setParam: (paramField: string, paramValue: unknown) => void;
}
const isSearchSourceParam = (action: LocalStateAction): action is SearchSourceParamsAction => {
return action.type === 'filter' || action.type === 'index' || action.type === 'query';
};
export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProps) => {
const { data } = useTriggersAndActionsUiDeps();
const { searchSource, ruleParams, errors, initialSavedQuery, setParam } = props;
const { thresholdComparator, timeWindowUnit } = ruleParams;
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []);
useEffect(() => setSavedQuery(initialSavedQuery), [initialSavedQuery]);
const [{ index: dataView, query, filter: filters, threshold, timeWindowSize, size }, dispatch] =
useReducer<LocalStateReducer>(
(currentState, action) => {
if (isSearchSourceParam(action)) {
searchSource.setParent(undefined).setField(action.type, action.payload);
setParam('searchConfiguration', searchSource.getSerializedFields());
} else {
setParam(action.type, action.payload);
}
return { ...currentState, [action.type]: action.payload };
},
{
index: searchSource.getField('index')!,
query: searchSource.getField('query')!,
filter: mapAndFlattenFilters(searchSource.getField('filter') as Filter[]),
threshold: ruleParams.threshold,
timeWindowSize: ruleParams.timeWindowSize,
size: ruleParams.size,
}
);
const dataViews = useMemo(() => [dataView], [dataView]);
const onSelectDataView = useCallback(
(newDataViewId) =>
data.dataViews
.get(newDataViewId)
.then((newDataView) => dispatch({ type: 'index', payload: newDataView })),
[data.dataViews]
);
const onUpdateFilters = useCallback((newFilters) => {
dispatch({ type: 'filter', payload: mapAndFlattenFilters(newFilters) });
}, []);
const onChangeQuery = useCallback(
({ query: newQuery }: { query?: Query }) => {
if (!deepEqual(newQuery, query)) {
dispatch({ type: 'query', payload: newQuery || { ...query, query: '' } });
}
},
[query]
);
// needs to change language mode only
const onQueryBarSubmit = ({ query: newQuery }: { query?: Query }) => {
if (newQuery?.language !== query.language) {
dispatch({ type: 'query', payload: { ...query, language: newQuery?.language } as Query });
}
};
// Saved query
const onSavedQuery = useCallback((newSavedQuery: SavedQuery) => {
setSavedQuery(newSavedQuery);
const newFilters = newSavedQuery.attributes.filters;
if (newFilters) {
dispatch({ type: 'filter', payload: newFilters });
}
}, []);
const onClearSavedQuery = () => {
setSavedQuery(undefined);
dispatch({ type: 'query', payload: { ...query, query: '' } });
};
// window size
const onChangeWindowUnit = useCallback(
(selectedWindowUnit: string) => setParam('timeWindowUnit', selectedWindowUnit),
[setParam]
);
const onChangeWindowSize = useCallback(
(selectedWindowSize?: number) =>
selectedWindowSize && dispatch({ type: 'timeWindowSize', payload: selectedWindowSize }),
[]
);
// threshold
const onChangeSelectedThresholdComparator = useCallback(
(selectedThresholdComparator?: string) =>
setParam('thresholdComparator', selectedThresholdComparator),
[setParam]
);
const onChangeSelectedThreshold = useCallback(
(selectedThresholds?: number[]) =>
selectedThresholds && dispatch({ type: 'threshold', payload: selectedThresholds }),
[]
);
const onChangeSizeValue = useCallback(
(updatedValue: number) => dispatch({ type: 'size', payload: updatedValue }),
[]
);
return (
<Fragment>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchThreshold.ui.conditionPrompt"
defaultMessage="When the number of documents match"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<DataViewSelectPopover
initialDataViewTitle={dataView.title}
initialDataViewId={dataView.id}
onSelectDataView={onSelectDataView}
/>
<EuiSpacer size="s" />
<SearchBar
onQuerySubmit={onQueryBarSubmit}
onQueryChange={onChangeQuery}
suggestionsSize="s"
displayStyle="inPage"
placeholder={i18n.translate('xpack.stackAlerts.searchSource.ui.searchQuery', {
defaultMessage: 'Search query',
})}
query={query}
indexPatterns={dataViews}
savedQuery={savedQuery}
filters={filters}
onFiltersUpdated={onUpdateFilters}
onClearSavedQuery={onClearSavedQuery}
onSavedQueryUpdated={onSavedQuery}
onSaved={onSavedQuery}
showSaveQuery={true}
showQueryBar={true}
showQueryInput={true}
showFilterBar={true}
showDatePicker={false}
showAutoRefreshOnly={false}
showSubmitButton={false}
dateRangeFrom={undefined}
dateRangeTo={undefined}
timeHistory={timeHistory}
hiddenFilterPanelOptions={['pinFilter', 'disableFilter']}
/>
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchSource.ui.conditionPrompt"
defaultMessage="When the number of matches"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ThresholdExpression
data-test-subj="thresholdExpression"
thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR}
threshold={threshold ?? DEFAULT_VALUES.THRESHOLD}
errors={errors}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedThreshold={onChangeSelectedThreshold}
onChangeSelectedThresholdComparator={onChangeSelectedThresholdComparator}
/>
<ForLastExpression
data-test-subj="forLastExpression"
popupPosition={'upLeft'}
timeWindowSize={timeWindowSize}
timeWindowUnit={timeWindowUnit}
display="fullWidth"
errors={errors}
onChangeWindowSize={onChangeWindowSize}
onChangeWindowUnit={onChangeWindowUnit}
/>
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchSource.ui.selectSizePrompt"
defaultMessage="Select a size"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ValueExpression
description={i18n.translate('xpack.stackAlerts.searchSource.ui.sizeExpression', {
defaultMessage: 'Size',
})}
data-test-subj="sizeValueExpression"
value={size}
errors={errors.size}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedValue={onChangeSizeValue}
/>
<EuiSpacer size="s" />
</Fragment>
);
};

View file

@ -7,6 +7,9 @@
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { EuiComboBoxOptionOption } from '@elastic/eui';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { EXPRESSION_ERRORS } from './constants';
export interface Comparator {
text: string;
@ -19,7 +22,7 @@ export enum SearchType {
searchSource = 'searchSource',
}
export interface CommonAlertParams<T extends SearchType> extends RuleTypeParams {
export interface CommonAlertParams extends RuleTypeParams {
size: number;
thresholdComparator?: string;
threshold: number[];
@ -28,8 +31,8 @@ export interface CommonAlertParams<T extends SearchType> extends RuleTypeParams
}
export type EsQueryAlertParams<T = SearchType> = T extends SearchType.searchSource
? CommonAlertParams<SearchType.searchSource> & OnlySearchSourceAlertParams
: CommonAlertParams<SearchType.esQuery> & OnlyEsQueryAlertParams;
? CommonAlertParams & OnlySearchSourceAlertParams
: CommonAlertParams & OnlyEsQueryAlertParams;
export interface OnlyEsQueryAlertParams {
esQuery: string;
@ -39,4 +42,15 @@ export interface OnlyEsQueryAlertParams {
export interface OnlySearchSourceAlertParams {
searchType: 'searchSource';
searchConfiguration: SerializedSearchSourceFields;
savedQueryId?: string;
}
export type DataViewOption = EuiComboBoxOptionOption<string>;
export type ExpressionErrors = typeof EXPRESSION_ERRORS;
export type ErrorKey = keyof ExpressionErrors & unknown;
export interface TriggersAndActionsUiDeps {
data: DataPublicPluginStart;
}

View file

@ -5,10 +5,13 @@
* 2.0.
*/
import { EsQueryAlertParams, SearchType } from './types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { EsQueryAlertParams, SearchType, TriggersAndActionsUiDeps } from './types';
export const isSearchSourceAlert = (
ruleParams: EsQueryAlertParams
): ruleParams is EsQueryAlertParams<SearchType.searchSource> => {
return ruleParams.searchType === 'searchSource';
};
export const useTriggersAndActionsUiDeps = () => useKibana<TriggersAndActionsUiDeps>().services;

View file

@ -5,25 +5,17 @@
* 2.0.
*/
import { defaultsDeep } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ValidationResult, builtInComparators } from '@kbn/triggers-actions-ui-plugin/public';
import { EsQueryAlertParams } from './types';
import { EsQueryAlertParams, ExpressionErrors } from './types';
import { isSearchSourceAlert } from './util';
import { EXPRESSION_ERRORS } from './constants';
export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => {
const { size, threshold, timeWindowSize, thresholdComparator } = alertParams;
const validationResult = { errors: {} };
const errors = {
index: new Array<string>(),
timeField: new Array<string>(),
esQuery: new Array<string>(),
size: new Array<string>(),
threshold0: new Array<string>(),
threshold1: new Array<string>(),
thresholdComparator: new Array<string>(),
timeWindowSize: new Array<string>(),
searchConfiguration: new Array<string>(),
};
const errors: ExpressionErrors = defaultsDeep({}, EXPRESSION_ERRORS);
validationResult.errors = errors;
if (!threshold || threshold.length === 0 || threshold[0] === undefined) {
errors.threshold0.push(

View file

@ -30,6 +30,7 @@ exports[`should render BoundaryIndexExpression 1`] = `
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"getIdsWithTitle": [MockFunction],
"make": [Function],
}
}
@ -106,6 +107,7 @@ exports[`should render EntityIndexExpression 1`] = `
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"getIdsWithTitle": [MockFunction],
"make": [Function],
}
}
@ -188,6 +190,7 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = `
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"getIdsWithTitle": [MockFunction],
"make": [Function],
}
}

View file

@ -20,6 +20,7 @@ describe('ActionContext', () => {
timeWindowUnit: 'm',
thresholdComparator: '>',
threshold: [4],
searchType: 'esQuery',
}) as OnlyEsQueryAlertParams;
const base: EsQueryAlertActionContext = {
date: '2020-01-01T00:00:00.000Z',
@ -50,6 +51,7 @@ describe('ActionContext', () => {
timeWindowUnit: 'm',
thresholdComparator: 'between',
threshold: [4, 5],
searchType: 'esQuery',
}) as OnlyEsQueryAlertParams;
const base: EsQueryAlertActionContext = {
date: '2020-01-01T00:00:00.000Z',

View file

@ -110,6 +110,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.LT,
threshold: [0],
searchType: 'esQuery',
};
expect(alertType.validate?.params?.validate(params)).toBeTruthy();
@ -128,6 +129,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.BETWEEN,
threshold: [0],
searchType: 'esQuery',
};
expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot(
@ -145,6 +147,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.BETWEEN,
threshold: [0],
searchType: 'esQuery',
};
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -174,6 +177,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
};
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -219,6 +223,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
};
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -267,6 +272,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
};
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -309,6 +315,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
};
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -380,6 +387,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
};
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();
@ -425,6 +433,7 @@ describe('alertType', () => {
timeWindowUnit: 'm',
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
};
const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices();

View file

@ -7,10 +7,12 @@
import { i18n } from '@kbn/i18n';
import { CoreSetup, Logger } from '@kbn/core/server';
import { extractReferences, injectReferences } from '@kbn/data-plugin/common';
import { RuleType } from '../../types';
import { ActionContext } from './action_context';
import {
EsQueryAlertParams,
EsQueryAlertParamsExtractedParams,
EsQueryAlertParamsSchema,
EsQueryAlertState,
} from './alert_type_params';
@ -18,13 +20,14 @@ import { STACK_ALERTS_FEATURE_ID } from '../../../common';
import { ExecutorOptions } from './types';
import { ActionGroupId, ES_QUERY_ID } from './constants';
import { executor } from './executor';
import { isEsQueryAlert } from './util';
export function getAlertType(
logger: Logger,
core: CoreSetup
): RuleType<
EsQueryAlertParams,
never, // Only use if defining useSavedObjectReferences hook
EsQueryAlertParamsExtractedParams,
EsQueryAlertState,
{},
ActionContext,
@ -159,6 +162,25 @@ export function getAlertType(
{ name: 'index', description: actionVariableContextIndexLabel },
],
},
useSavedObjectReferences: {
extractReferences: (params) => {
if (isEsQueryAlert(params.searchType)) {
return { params: params as EsQueryAlertParamsExtractedParams, references: [] };
}
const [searchConfiguration, references] = extractReferences(params.searchConfiguration);
const newParams = { ...params, searchConfiguration } as EsQueryAlertParamsExtractedParams;
return { params: newParams, references };
},
injectReferences: (params, references) => {
if (isEsQueryAlert(params.searchType)) {
return params;
}
return {
...params,
searchConfiguration: injectReferences(params.searchConfiguration, references),
};
},
},
minimumLicenseRequired: 'basic',
isExportable: true,
executor: async (options: ExecutorOptions<EsQueryAlertParams>) => {

View file

@ -23,6 +23,7 @@ const DefaultParams: Writable<Partial<EsQueryAlertParams>> = {
timeWindowUnit: 'm',
thresholdComparator: Comparator.GT,
threshold: [0],
searchType: 'esQuery',
};
describe('alertType Params validate()', () => {

View file

@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import { validateTimeWindowUnits } from '@kbn/triggers-actions-ui-plugin/server';
import { RuleTypeState } from '@kbn/alerting-plugin/server';
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { Comparator } from '../../../common/comparator_types';
import { ComparatorFnNames } from '../lib';
import { getComparatorSchemaType } from '../lib/comparator';
@ -21,13 +22,21 @@ export interface EsQueryAlertState extends RuleTypeState {
latestTimestamp: string | undefined;
}
export type EsQueryAlertParamsExtractedParams = Omit<EsQueryAlertParams, 'searchConfiguration'> & {
searchConfiguration: SerializedSearchSourceFields & {
indexRefName: string;
};
};
const EsQueryAlertParamsSchemaProperties = {
size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }),
timeWindowSize: schema.number({ min: 1 }),
timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }),
threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }),
thresholdComparator: getComparatorSchemaType(validateComparator),
searchType: schema.nullable(schema.literal('searchSource')),
searchType: schema.oneOf([schema.literal('searchSource'), schema.literal('esQuery')], {
defaultValue: 'esQuery',
}),
// searchSource alert param only
searchConfiguration: schema.conditional(
schema.siblingRef('searchType'),
@ -38,21 +47,21 @@ const EsQueryAlertParamsSchemaProperties = {
// esQuery alert params only
esQuery: schema.conditional(
schema.siblingRef('searchType'),
schema.literal('searchSource'),
schema.never(),
schema.string({ minLength: 1 })
schema.literal('esQuery'),
schema.string({ minLength: 1 }),
schema.never()
),
index: schema.conditional(
schema.siblingRef('searchType'),
schema.literal('searchSource'),
schema.never(),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 })
schema.literal('esQuery'),
schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
schema.never()
),
timeField: schema.conditional(
schema.siblingRef('searchType'),
schema.literal('searchSource'),
schema.never(),
schema.string({ minLength: 1 })
schema.literal('esQuery'),
schema.string({ minLength: 1 }),
schema.never()
),
};

View file

@ -18,6 +18,7 @@ describe('es_query executor', () => {
esQuery: '{ "query": "test-query" }',
index: ['test-index'],
timeField: '',
searchType: 'esQuery',
};
describe('tryToParseAsDate', () => {
it.each<[string | number]>([['2019-01-01T00:00:00.000Z'], [1546300800000]])(

View file

@ -16,13 +16,14 @@ import { fetchEsQuery } from './lib/fetch_es_query';
import { EsQueryAlertParams } from './alert_type_params';
import { fetchSearchSourceQuery } from './lib/fetch_search_source_query';
import { Comparator } from '../../../common/comparator_types';
import { isEsQueryAlert } from './util';
export async function executor(
logger: Logger,
core: CoreSetup,
options: ExecutorOptions<EsQueryAlertParams>
) {
const esQueryAlert = isEsQueryAlert(options);
const esQueryAlert = isEsQueryAlert(options.params.searchType);
const { alertId, name, services, params, state } = options;
const { alertFactory, scopedClusterClient, searchSourceClient } = services;
const currentTimestamp = new Date().toISOString();
@ -162,10 +163,6 @@ export function tryToParseAsDate(sortValue?: string | number | null): undefined
}
}
export function isEsQueryAlert(options: ExecutorOptions<EsQueryAlertParams>) {
return options.params.searchType !== 'searchSource';
}
export function getChecksum(params: EsQueryAlertParams) {
return sha256.create().update(JSON.stringify(params));
}

View file

@ -10,7 +10,9 @@ import { ActionContext } from './action_context';
import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params';
import { ActionGroupId } from './constants';
export type OnlyEsQueryAlertParams = Omit<EsQueryAlertParams, 'searchConfiguration' | 'searchType'>;
export type OnlyEsQueryAlertParams = Omit<EsQueryAlertParams, 'searchConfiguration'> & {
searchType: 'esQuery';
};
export type OnlySearchSourceAlertParams = Omit<
EsQueryAlertParams,

View file

@ -0,0 +1,12 @@
/*
* 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 { EsQueryAlertParams } from './alert_type_params';
export function isEsQueryAlert(searchType: EsQueryAlertParams['searchType']) {
return searchType !== 'searchSource';
}