mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
d34408876a
commit
bc31053dc9
34 changed files with 800 additions and 416 deletions
|
@ -14,7 +14,8 @@
|
|||
"triggersActionsUi",
|
||||
"kibanaReact",
|
||||
"savedObjects",
|
||||
"data"
|
||||
"data",
|
||||
"kibanaUtils"
|
||||
],
|
||||
"configPath": ["xpack", "stack_alerts"],
|
||||
"requiredBundles": ["esUiShared"],
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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[];
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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>) => {
|
||||
|
|
|
@ -23,6 +23,7 @@ const DefaultParams: Writable<Partial<EsQueryAlertParams>> = {
|
|||
timeWindowUnit: 'm',
|
||||
thresholdComparator: Comparator.GT,
|
||||
threshold: [0],
|
||||
searchType: 'esQuery',
|
||||
};
|
||||
|
||||
describe('alertType Params validate()', () => {
|
||||
|
|
|
@ -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()
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
@ -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]])(
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue