diff --git a/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx b/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx index 26007ca9be712..e63e456c10f76 100644 --- a/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx +++ b/src/plugins/unified_search/public/dataview_picker/explore_matching_button.tsx @@ -44,6 +44,7 @@ export const ExploreMatchingButton = ({ { setPopoverIsOpen(false); onCreateDefaultAdHocDataView(dataViewSearchString); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts index efb8144c2a840..7013f2099b8b4 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/constants.ts @@ -6,7 +6,6 @@ */ import { COMPARATORS } from '@kbn/triggers-actions-ui-plugin/public'; -import { ErrorKey } from './types'; export const DEFAULT_VALUES = { THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, @@ -22,17 +21,31 @@ export const DEFAULT_VALUES = { EXCLUDE_PREVIOUS_HITS: true, }; -export const EXPRESSION_ERRORS = { - index: new Array(), - size: new Array(), - timeField: new Array(), +export const COMMON_EXPRESSION_ERRORS = { + searchType: new Array(), threshold0: new Array(), threshold1: new Array(), - esQuery: new Array(), - thresholdComparator: new Array(), timeWindowSize: new Array(), + size: new Array(), +}; + +export const SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS = { searchConfiguration: new Array(), - searchType: new Array(), + timeField: new Array(), +}; + +export const ONLY_ES_QUERY_EXPRESSION_ERRORS = { + index: new Array(), + esQuery: new Array(), + timeField: new Array(), +}; + +const ALL_EXPRESSION_ERROR_ENTRIES = { + ...COMMON_EXPRESSION_ERRORS, + ...SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS, + ...ONLY_ES_QUERY_EXPRESSION_ERRORS, }; -export const EXPRESSION_ERROR_KEYS = Object.keys(EXPRESSION_ERRORS) as ErrorKey[]; +export const ALL_EXPRESSION_ERROR_KEYS = Object.keys(ALL_EXPRESSION_ERROR_ENTRIES) as Array< + keyof typeof ALL_EXPRESSION_ERROR_ENTRIES +>; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.tsx index 359faa935346b..7c0c5a6d2b5bb 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.tsx @@ -11,10 +11,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFormRow, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { DocLinksStart, HttpSetup } from '@kbn/core/public'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; -import { CodeEditor, useKibana } from '@kbn/kibana-react-plugin/public'; +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { getFields, RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; import { parseDuration } from '@kbn/alerting-plugin/common'; import { hasExpressionValidationErrors } from '../validation'; @@ -24,14 +23,10 @@ import { IndexSelectPopover } from '../../components/index_select_popover'; import { DEFAULT_VALUES } from '../constants'; import { RuleCommonExpressions } from '../rule_common_expressions'; import { totalHitsToNumber } from '../test_query_row'; +import { useTriggerUiActionServices } from '../util'; const { useXJsonMode } = XJson; -interface KibanaDeps { - http: HttpSetup; - docLinks: DocLinksStart; -} - export const EsQueryExpression: React.FC< RuleTypeParamsExpressionProps, EsQueryRuleMetaData> > = ({ ruleParams, setRuleParams, setRuleProperty, errors, data }) => { @@ -73,7 +68,8 @@ export const EsQueryExpression: React.FC< [setRuleParams] ); - const { http, docLinks } = useKibana().services; + const services = useTriggerUiActionServices(); + const { http, docLinks } = services; const [esFields, setEsFields] = useState< Array<{ @@ -107,13 +103,9 @@ export const EsQueryExpression: React.FC< } }; - const hasValidationErrors = useCallback(() => { - return hasExpressionValidationErrors(currentRuleParams); - }, [currentRuleParams]); - const onTestQuery = useCallback(async () => { const window = `${timeWindowSize}${timeWindowUnit}`; - if (hasValidationErrors()) { + if (hasExpressionValidationErrors(currentRuleParams)) { return { nrOfDocs: 0, timeWindow: window }; } const timeWindow = parseDuration(window); @@ -136,7 +128,7 @@ export const EsQueryExpression: React.FC< const hits = rawResponse.hits; return { nrOfDocs: totalHitsToNumber(hits.total), timeWindow: window }; - }, [data.search, esQuery, index, timeField, timeWindowSize, timeWindowUnit, hasValidationErrors]); + }, [timeWindowSize, timeWindowUnit, currentRuleParams, esQuery, data.search, index, timeField]); return ( @@ -251,7 +243,7 @@ export const EsQueryExpression: React.FC< setParam('size', updatedValue); }} errors={errors} - hasValidationErrors={hasValidationErrors()} + hasValidationErrors={hasExpressionValidationErrors(currentRuleParams)} onTestFetch={onTestQuery} excludeHitsFromPreviousRun={excludeHitsFromPreviousRun} onChangeExcludeHitsFromPreviousRun={(exclude) => { diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx index 65cd4b8e1a65c..30045fee81a29 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx @@ -16,7 +16,7 @@ import { SearchSourceExpression, SearchSourceExpressionProps } from './search_so import { EsQueryExpression } from './es_query_expression'; import { QueryFormTypeChooser } from './query_form_type_chooser'; import { isSearchSourceRule } from '../util'; -import { EXPRESSION_ERROR_KEYS } from '../constants'; +import { ALL_EXPRESSION_ERROR_KEYS } from '../constants'; function areSearchSourceExpressionPropsEqual( prevProps: Readonly>, @@ -59,7 +59,7 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent< } ); - const errorParam = EXPRESSION_ERROR_KEYS.find((errorKey) => { + const errorParam = ALL_EXPRESSION_ERROR_KEYS.find((errorKey) => { return errors[errorKey]?.length >= 1 && ruleParams[errorKey] !== undefined; }); @@ -68,8 +68,9 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent< (); const setParam = useCallback( - (paramField: string, paramValue: unknown) => setRuleParams(paramField, paramValue), + (paramField: string, paramValue: unknown) => { + setRuleParams(paramField, paramValue); + }, [setRuleParams] ); @@ -65,22 +67,26 @@ export const SearchSourceExpression = ({ initialSearchConfiguration = newSearchSource.getSerializedFields(); } - setRuleProperty('params', { - searchConfiguration: initialSearchConfiguration, - 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, - excludeHitsFromPreviousRun: - excludeHitsFromPreviousRun ?? DEFAULT_VALUES.EXCLUDE_PREVIOUS_HITS, - }); - - data.search.searchSource - .create(initialSearchConfiguration) - .then(setSearchSource) - .catch(setParamsError); + try { + const createdSearchSource = await data.search.searchSource.create( + initialSearchConfiguration + ); + setRuleProperty('params', { + searchConfiguration: initialSearchConfiguration, + timeField: createdSearchSource.getField('index')?.timeFieldName, + 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, + excludeHitsFromPreviousRun: + excludeHitsFromPreviousRun ?? DEFAULT_VALUES.EXCLUDE_PREVIOUS_HITS, + }); + setSearchSource(createdSearchSource); + } catch (error) { + setParamsError(error); + } }; initSearchSource(); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx index a439748500a48..97edd294df28a 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx @@ -26,8 +26,8 @@ import { DEFAULT_VALUES } from '../constants'; import { DataViewSelectPopover } from '../../components/data_view_select_popover'; import { RuleCommonExpressions } from '../rule_common_expressions'; import { totalHitsToNumber } from '../test_query_row'; -import { hasExpressionValidationErrors } from '../validation'; import { useTriggerUiActionServices } from '../util'; +import { hasExpressionValidationErrors } from '../validation'; const HIDDEN_FILTER_PANEL_OPTIONS: SearchBarProps['hiddenFilterPanelOptions'] = [ 'pinFilter', @@ -94,6 +94,10 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp if (isSearchSourceParam(action)) { searchSource.setParent(undefined).setField(action.type, action.payload); setParam('searchConfiguration', searchSource.getSerializedFields()); + + if (action.type === 'index') { + setParam('timeField', searchSource.getField('index')?.timeFieldName); + } } else { setParam(action.type, action.payload); } @@ -112,6 +116,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp ruleParams.excludeHitsFromPreviousRun ?? DEFAULT_VALUES.EXCLUDE_PREVIOUS_HITS, } ); + const { index: dataView, query, filter: filters } = ruleConfiguration; const dataViews = useMemo(() => (dataView ? [dataView] : []), [dataView]); @@ -290,7 +295,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp onChangeWindowUnit={onChangeWindowUnit} onChangeSizeValue={onChangeSizeValue} errors={errors} - hasValidationErrors={hasExpressionValidationErrors(ruleParams) || !dataView} + hasValidationErrors={hasExpressionValidationErrors(props.ruleParams)} onTestFetch={onTestFetch} onCopyQuery={onCopyQuery} excludeHitsFromPreviousRun={ruleConfiguration.excludeHitsFromPreviousRun} diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts index 23cb6d8fc6e82..ab096a8d0fe68 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts @@ -8,11 +8,7 @@ import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; -import { EXPRESSION_ERRORS } from './constants'; +import type { DataView } from '@kbn/data-views-plugin/public'; export interface Comparator { text: string; @@ -50,20 +46,10 @@ export interface OnlyEsQueryRuleParams { } export interface OnlySearchSourceRuleParams { + timeField?: string; searchType?: 'searchSource'; searchConfiguration?: SerializedSearchSourceFields; savedQueryId?: string; } export type DataViewOption = EuiComboBoxOptionOption; - -export type ExpressionErrors = typeof EXPRESSION_ERRORS; - -export type ErrorKey = keyof ExpressionErrors & unknown; - -export interface TriggersAndActionsUiDeps { - dataViews: DataViewsPublicPluginStart; - unifiedSearch: UnifiedSearchPublicPluginStart; - data: DataPublicPluginStart; - dataViewEditor: DataViewEditorStart; -} diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts index cc4cfb2313d69..dfb833ef93a0f 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/util.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { EsQueryRuleParams, SearchType, TriggersAndActionsUiDeps } from './types'; +import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; +import { EsQueryRuleParams, SearchType } from './types'; export const isSearchSourceRule = ( ruleParams: EsQueryRuleParams @@ -14,4 +14,4 @@ export const isSearchSourceRule = ( return ruleParams.searchType === 'searchSource'; }; -export const useTriggerUiActionServices = () => useKibana().services; +export const useTriggerUiActionServices = () => useKibana().services; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts index bbfd8cdd8e698..e287d30fdc034 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/validation.ts @@ -8,15 +8,17 @@ import { defaultsDeep, isNil } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ValidationResult, builtInComparators } from '@kbn/triggers-actions-ui-plugin/public'; -import { EsQueryRuleParams, ExpressionErrors } from './types'; +import { EsQueryRuleParams, SearchType } from './types'; import { isSearchSourceRule } from './util'; -import { EXPRESSION_ERRORS } from './constants'; +import { + COMMON_EXPRESSION_ERRORS, + ONLY_ES_QUERY_EXPRESSION_ERRORS, + SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS, +} from './constants'; -export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationResult => { +const validateCommonParams = (ruleParams: EsQueryRuleParams) => { const { size, threshold, timeWindowSize, thresholdComparator } = ruleParams; - const validationResult = { errors: {} }; - const errors: ExpressionErrors = defaultsDeep({}, EXPRESSION_ERRORS); - validationResult.errors = errors; + const errors: typeof COMMON_EXPRESSION_ERRORS = defaultsDeep({}, COMMON_EXPRESSION_ERRORS); if (!('index' in ruleParams) && !ruleParams.searchType) { errors.searchType.push( @@ -25,7 +27,7 @@ export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationRes }) ); - return validationResult; + return errors; } if (!threshold || threshold.length === 0 || threshold[0] === undefined) { @@ -79,43 +81,54 @@ export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationRes ); } - /** - * Skip esQuery and index params check if it is search source rule, - * since it should contain searchConfiguration instead of esQuery and index. - */ - const isSearchSource = isSearchSourceRule(ruleParams); - if (isSearchSource) { - if (!ruleParams.searchConfiguration) { - errors.searchConfiguration.push( - i18n.translate( - 'xpack.stackAlerts.esQuery.ui.validation.error.requiredSearchConfiguration', - { - defaultMessage: 'Search source configuration is required.', - } - ) - ); - } else if (!ruleParams.searchConfiguration.index) { - errors.index.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredDataViewText', { - defaultMessage: 'Data view is required.', - }) - ); - } else if ( - typeof ruleParams.searchConfiguration.index === 'object' && - !Object.hasOwn(ruleParams.searchConfiguration.index, 'timeFieldName') - ) { - errors.index.push( - i18n.translate( - 'xpack.stackAlerts.esQuery.ui.validation.error.requiredDataViewTimeFieldText', - { - defaultMessage: 'Data view should have a time field.', - } - ) - ); - } - return validationResult; + return errors; +}; + +const validateSearchSourceParams = (ruleParams: EsQueryRuleParams) => { + const errors: typeof SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS = defaultsDeep( + {}, + SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS + ); + + if (!ruleParams.searchConfiguration) { + errors.searchConfiguration.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSearchConfiguration', { + defaultMessage: 'Search source configuration is required.', + }) + ); + return errors; + } + + if (!ruleParams.searchConfiguration.index) { + errors.searchConfiguration.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredDataViewText', { + defaultMessage: 'Data view is required.', + }) + ); + return errors; + } + + if (!ruleParams.timeField) { + errors.timeField.push( + i18n.translate( + 'xpack.stackAlerts.esQuery.ui.validation.error.requiredDataViewTimeFieldText', + { + defaultMessage: 'Data view should have a time field.', + } + ) + ); + return errors; } + return errors; +}; + +const validateEsQueryParams = (ruleParams: EsQueryRuleParams) => { + const errors: typeof ONLY_ES_QUERY_EXPRESSION_ERRORS = defaultsDeep( + {}, + ONLY_ES_QUERY_EXPRESSION_ERRORS + ); + if (!ruleParams.index || ruleParams.index.length === 0) { errors.index.push( i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredIndexText', { @@ -156,7 +169,33 @@ export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationRes ); } } + return errors; +}; + +export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationResult => { + const validationResult = { errors: {} }; + + const commonErrors = validateCommonParams(ruleParams); + validationResult.errors = commonErrors; + + /** + * Skip esQuery and index params check if it is search source rule, + * since it should contain searchConfiguration instead of esQuery and index. + * + * It's important to report searchSource rule related errors only into errors.searchConfiguration prop. + * For example errors.index is a mistake to report searchSource rule related errors. It will lead to issues. + */ + const isSearchSource = isSearchSourceRule(ruleParams); + if (isSearchSource) { + validationResult.errors = { + ...validationResult.errors, + ...validateSearchSourceParams(ruleParams), + }; + return validationResult; + } + const esQueryErrors = validateEsQueryParams(ruleParams as EsQueryRuleParams); + validationResult.errors = { ...validationResult.errors, ...esQueryErrors }; return validationResult; }; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts index 5aa40290c2f44..c9aa81223c6f3 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.test.ts @@ -6,8 +6,7 @@ */ import { EsQueryRuleActionContext, addMessages } from './action_context'; -import { EsQueryRuleParamsSchema } from './rule_type_params'; -import { OnlyEsQueryRuleParams } from './types'; +import { EsQueryRuleParams, EsQueryRuleParamsSchema } from './rule_type_params'; describe('ActionContext', () => { it('generates expected properties', async () => { @@ -21,7 +20,7 @@ describe('ActionContext', () => { thresholdComparator: '>', threshold: [4], searchType: 'esQuery', - }) as OnlyEsQueryRuleParams; + }) as EsQueryRuleParams; const base: EsQueryRuleActionContext = { date: '2020-01-01T00:00:00.000Z', value: 42, @@ -52,7 +51,7 @@ describe('ActionContext', () => { thresholdComparator: '>', threshold: [4], searchType: 'esQuery', - }) as OnlyEsQueryRuleParams; + }) as EsQueryRuleParams; const base: EsQueryRuleActionContext = { date: '2020-01-01T00:00:00.000Z', value: 42, @@ -83,7 +82,7 @@ describe('ActionContext', () => { thresholdComparator: 'between', threshold: [4, 5], searchType: 'esQuery', - }) as OnlyEsQueryRuleParams; + }) as EsQueryRuleParams; const base: EsQueryRuleActionContext = { date: '2020-01-01T00:00:00.000Z', value: 4, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts index 1dc5b5422e121..38a16dacf9c53 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/action_context.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { AlertInstanceContext } from '@kbn/alerting-plugin/server'; -import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; +import { EsQueryRuleParams } from './rule_type_params'; // rule type context provided to actions export interface ActionContext extends EsQueryRuleActionContext { @@ -35,7 +35,7 @@ export interface EsQueryRuleActionContext extends AlertInstanceContext { export function addMessages( ruleName: string, baseContext: EsQueryRuleActionContext, - params: OnlyEsQueryRuleParams | OnlySearchSourceRuleParams, + params: EsQueryRuleParams, isRecovered: boolean = false ): ActionContext { const title = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle', { diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts index 127224f1bf1b6..5b6d12ee1222a 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts @@ -38,6 +38,7 @@ const defaultParams: OnlySearchSourceRuleParams = { searchConfiguration: {}, searchType: 'searchSource', excludeHitsFromPreviousRun: true, + timeField: 'time', }; describe('fetchSearchSourceQuery', () => { diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts index 81f6248d906ca..c14e57c5414d3 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts @@ -54,9 +54,8 @@ export function updateSearchSource( params: OnlySearchSourceRuleParams, latestTimestamp: string | undefined ) { - const index = searchSource.getField('index'); - - const timeFieldName = index?.timeFieldName; + const index = searchSource.getField('index')!; + const timeFieldName = params.timeField || index.timeFieldName; if (!timeFieldName) { throw new Error('Invalid data view without timeFieldName.'); } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts index 941a45d3a9a1e..d62afa9abd7f7 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type_params.ts @@ -38,6 +38,12 @@ const EsQueryRuleParamsSchemaProperties = { searchType: schema.oneOf([schema.literal('searchSource'), schema.literal('esQuery')], { defaultValue: 'esQuery', }), + timeField: schema.conditional( + schema.siblingRef('searchType'), + schema.literal('esQuery'), + schema.string({ minLength: 1 }), + schema.maybe(schema.string({ minLength: 1 })) + ), // searchSource rule param only searchConfiguration: schema.conditional( schema.siblingRef('searchType'), @@ -58,12 +64,6 @@ const EsQueryRuleParamsSchemaProperties = { schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), schema.never() ), - timeField: schema.conditional( - schema.siblingRef('searchType'), - schema.literal('esQuery'), - schema.string({ minLength: 1 }), - schema.never() - ), }; export const EsQueryRuleParamsSchema = schema.object(EsQueryRuleParamsSchemaProperties, { diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts index 2b0f0f7a7407c..c8844b19a678a 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts @@ -12,12 +12,10 @@ import { ActionGroupId } from './constants'; export type OnlyEsQueryRuleParams = Omit & { searchType: 'esQuery'; + timeField: string; }; -export type OnlySearchSourceRuleParams = Omit< - EsQueryRuleParams, - 'esQuery' | 'index' | 'timeField' -> & { +export type OnlySearchSourceRuleParams = Omit & { searchType: 'searchSource'; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx index a03ba8dc92620..feec862ab804f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx @@ -78,6 +78,7 @@ export const RuleEdit = ({ http, notifications: { toasts }, } = useKibana().services; + const setRule = (value: Rule) => { dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index 22518669681ed..7efc5a0ca1ab8 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -169,7 +169,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { "alert_id": "{{alertId}}", "context_message": "{{context.message}}" }`); - await testSubjects.click('saveRuleButton'); }; const openDiscoverAlertFlyout = async () => { @@ -280,15 +279,40 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { outputDataViewId = outputDataViewResponse.body.data_view.id; }); - it('should navigate to alert results via view in app link', async () => { + it('should show time field validation error', async () => { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.selectIndexPattern(SOURCE_DATA_INDEX); await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes'); - // create an alert await openDiscoverAlertFlyout(); await defineSearchSourceAlert(RULE_NAME); + await testSubjects.click('selectDataViewExpression'); + + await testSubjects.click('indexPattern-switcher--input'); + const input = await find.activeElement(); + // search-source-alert-output index does not have time field + await input.type('search-source-alert-o*'); + await testSubjects.click('explore-matching-indices-button'); + + await testSubjects.click('saveRuleButton'); + + const errorElem = await testSubjects.find('esQueryAlertExpressionError'); + const errorText = await errorElem.getVisibleText(); + expect(errorText).to.eql('Data view should have a time field.'); + }); + + it('should navigate to alert results via view in app link', async () => { + await testSubjects.click('selectDataViewExpression'); + await testSubjects.click('indexPattern-switcher--input'); + const dataViewsElem = await testSubjects.find('euiSelectableList'); + const sourceDataViewOption = await dataViewsElem.findByCssSelector( + `[title="${SOURCE_DATA_INDEX}"]` + ); + await sourceDataViewOption.click(); + + await testSubjects.click('saveRuleButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); await openAlertRuleInManagement(RULE_NAME); @@ -403,6 +427,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // create an alert await openDiscoverAlertFlyout(); await defineSearchSourceAlert('test-adhoc-alert'); + await testSubjects.click('saveRuleButton'); await PageObjects.header.waitUntilLoadingHasFinished(); sourceAdHocDataViewId = await PageObjects.discover.getCurrentDataViewId();