Skip to content

Commit

Permalink
[Discover] Validate if Data View time field exists on Alert creation …
Browse files Browse the repository at this point in the history
…/ editing (#146324)

## Summary

Closes #135806

This PR adds optional `timeField` param for Discover alert and adding
validation data view if it time based.


![AD61D10F-6278-429C-B69D-C1952BB0A3C1_4_5005_c](https://user-images.githubusercontent.com/39378793/205312590-0392cd2e-740e-4e3e-ba17-712e0696eef3.jpeg)

### How to test
- Open `Alerts` in Discover
- Select non time based data view
- Try to save the rule. You should see error message.


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
dimaanj and kibanamachine authored Dec 14, 2022
1 parent 1f04bf8 commit 6b5ab8f
Show file tree
Hide file tree
Showing 17 changed files with 196 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const ExploreMatchingButton = ({
<EuiButton
fullWidth
size="s"
data-test-subj="explore-matching-indices-button"
onClick={() => {
setPopoverIsOpen(false);
onCreateDefaultAdHocDataView(dataViewSearchString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,17 +21,31 @@ export const DEFAULT_VALUES = {
EXCLUDE_PREVIOUS_HITS: true,
};

export const EXPRESSION_ERRORS = {
index: new Array<string>(),
size: new Array<string>(),
timeField: new Array<string>(),
export const COMMON_EXPRESSION_ERRORS = {
searchType: new Array<string>(),
threshold0: new Array<string>(),
threshold1: new Array<string>(),
esQuery: new Array<string>(),
thresholdComparator: new Array<string>(),
timeWindowSize: new Array<string>(),
size: new Array<string>(),
};

export const SEARCH_SOURCE_ONLY_EXPRESSION_ERRORS = {
searchConfiguration: new Array<string>(),
searchType: new Array<string>(),
timeField: new Array<string>(),
};

export const ONLY_ES_QUERY_EXPRESSION_ERRORS = {
index: new Array<string>(),
esQuery: new Array<string>(),
timeField: new Array<string>(),
};

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
>;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<EsQueryRuleParams<SearchType.esQuery>, EsQueryRuleMetaData>
> = ({ ruleParams, setRuleParams, setRuleProperty, errors, data }) => {
Expand Down Expand Up @@ -73,7 +68,8 @@ export const EsQueryExpression: React.FC<
[setRuleParams]
);

const { http, docLinks } = useKibana<KibanaDeps>().services;
const services = useTriggerUiActionServices();
const { http, docLinks } = services;

const [esFields, setEsFields] = useState<
Array<{
Expand Down Expand Up @@ -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);
Expand All @@ -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 (
<Fragment>
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropsWithChildren<SearchSourceExpressionProps>>,
Expand Down Expand Up @@ -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;
});

Expand All @@ -68,8 +68,9 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent<
<EuiCallOut
color="danger"
size="s"
data-test-subj="esQueryAlertExpressionError"
title={
['index', 'searchType'].includes(errorParam)
['index', 'searchType', 'timeField'].includes(errorParam)
? errors[errorParam]
: expressionGenericErrorMessage
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export const SearchSourceExpression = ({
const [paramsError, setParamsError] = useState<Error>();

const setParam = useCallback(
(paramField: string, paramValue: unknown) => setRuleParams(paramField, paramValue),
(paramField: string, paramValue: unknown) => {
setRuleParams(paramField, paramValue);
},
[setRuleParams]
);

Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
}
Expand All @@ -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]);

Expand Down Expand Up @@ -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}
Expand Down
18 changes: 2 additions & 16 deletions x-pack/plugins/stack_alerts/public/rule_types/es_query/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,20 +46,10 @@ export interface OnlyEsQueryRuleParams {
}

export interface OnlySearchSourceRuleParams {
timeField?: string;
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 {
dataViews: DataViewsPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
data: DataPublicPluginStart;
dataViewEditor: DataViewEditorStart;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
* 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
): ruleParams is EsQueryRuleParams<SearchType.searchSource> => {
return ruleParams.searchType === 'searchSource';
};

export const useTriggerUiActionServices = () => useKibana<TriggersAndActionsUiDeps>().services;
export const useTriggerUiActionServices = () => useKibana().services;
Loading

0 comments on commit 6b5ab8f

Please sign in to comment.