diff --git a/docs/user/alerting/images/alert-types-tracking-containment-conditions.png b/docs/user/alerting/images/alert-types-tracking-containment-conditions.png deleted file mode 100644 index b6b5dbf20ff35..0000000000000 Binary files a/docs/user/alerting/images/alert-types-tracking-containment-conditions.png and /dev/null differ diff --git a/docs/user/alerting/rule-types/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc index cfed5152e7657..f8c750acea62c 100644 --- a/docs/user/alerting/rule-types/geo-rule-types.asciidoc +++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc @@ -2,51 +2,25 @@ [[geo-alerting]] == Tracking containment -<> offers the tracking containment rule type which runs an {es} query over indices to determine whether any -documents are currently contained within any boundaries from the specified boundary index. -In the event that an entity is contained within a boundary, an alert may be generated. +The tracking containment rule alerts when an entity is contained or no longer contained within a boundary. [float] === Requirements To create a tracking containment rule, the following requirements must be present: -- *Tracks index or data view*: An index containing a `geo_point` or `geo_shape` field, `date` field, -and some form of entity identifier. An entity identifier is a `keyword` or `number` -field that consistently identifies the entity to be tracked. The data in this index should be dynamically -updating so that there are entity movements to alert upon. -- *Boundaries index or data view*: An index containing `geo_shape` data, such as boundary data and bounding box data. -This data is presumed to be static (not updating). Shape data matching the query is -harvested once when the rule is created and anytime after when the rule is re-enabled -after disablement. +- *Entities index*: An index containing a `geo_point` or `geo_shape` field, `date` field, and entity identifier. An entity identifier is a `keyword`, `number`, or `ip` field that identifies the entity. Entity data is expected to be updating so that there are entity movements to alert upon. +- *Boundaries index*: An index containing `geo_shape` data. +Boundaries data is expected to be static (not updating). Boundaries are collected once when the rule is created and anytime after when boundary configuration is modified. -By design, current interval entity locations (_current_ is determined by `date` in -the *Tracked index or data view*) are queried to determine if they are contained -within any monitored boundaries. Entity -data should be somewhat "real time", meaning the dates of new documents aren’t older +Entity locations are queried to determine if they are contained within any monitored boundaries. +Entity data should be somewhat "real time", meaning the dates of new documents aren’t older than the current time minus the amount of the interval. If data older than `now - ` is ingested, it won't trigger a rule. -[float] -=== Rule conditions - -Tracking containment rules have three clauses that define the condition to detect, -as well as two Kuery bars used to provide additional filtering context for each of the indices. - -[role="screenshot"] -image::user/alerting/images/alert-types-tracking-containment-conditions.png[Define the condition to detect,width=75%] -// NOTE: This is an autogenerated screenshot. Do not edit it directly. - -Index (entity):: This clause requires an *index or data view*, a *time field* that will be used for the *time window*, and a *`geo_point` or `geo_shape` field* for tracking. -Index (Boundary):: This clause requires an *index or data view*, a *`geo_shape` field* -identifying boundaries, and an optional *Human-readable boundary name* for better alerting -messages. - [float] === Actions -Conditions for how a rule is tracked can be specified uniquely for each individual action. -A rule can be triggered either when a containment condition is met or when an entity -is no longer contained. +A rule can be triggered either when a containment condition is met or when an entity is no longer contained. [role="screenshot"] image::user/alerting/images/alert-types-tracking-containment-action-options.png[Action frequency options for an action,width=75%] diff --git a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx index 7cd8b9d0251d8..f7148db93ce19 100644 --- a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx @@ -14,10 +14,7 @@ import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; export type IndexPatternSelectProps = Required< - Omit< - EuiComboBoxProps, - 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange' - >, + Omit, 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, 'placeholder' > & { onChange: (indexPatternId?: string) => void; @@ -155,7 +152,7 @@ export default class IndexPatternSelect extends Component { return { id: '.geo-containment', description: i18n.translate('xpack.stackAlerts.geoContainment.descriptionText', { - defaultMessage: 'Alert when an entity is contained within a geo boundary.', + defaultMessage: 'Alert when an entity is contained or no longer contained within a boundary.', }), iconClass: 'globe', documentationUrl: null, - ruleParamsExpression: lazy(() => import('./query_builder')), + ruleParamsExpression: lazy(() => import('./rule_form')), validate: validateExpression, requiresAppContext: false, }; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap deleted file mode 100644 index abef05ed9b343..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap +++ /dev/null @@ -1,278 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render BoundaryIndexExpression 1`] = ` - - - - - - - - - - - - } -/> -`; - -exports[`should render EntityIndexExpression 1`] = ` - - - - - - } - labelType="label" - > - - - - - - - } -/> -`; - -exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` - - - - - - } - labelType="label" - > - - - - - - - } -/> -`; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap deleted file mode 100644 index 16b77c6b2d5ab..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render entity by expression with aggregatable field options for entity 1`] = ` -
-
- -
-
-`; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx deleted file mode 100644 index f53b1bfab5982..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx +++ /dev/null @@ -1,176 +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, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { HttpSetup } from '@kbn/core/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; -import { DataViewField, DataView } from '@kbn/data-plugin/common'; -import { ES_GEO_SHAPE_TYPES, GeoContainmentAlertParams } from '../../types'; -import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; -import { SingleFieldSelect } from '../util_components/single_field_select'; -import { ExpressionWithPopover } from '../util_components/expression_with_popover'; - -interface Props { - ruleParams: GeoContainmentAlertParams; - errors: IErrorObject; - boundaryIndexPattern: DataView; - boundaryNameField?: string; - setBoundaryIndexPattern: (boundaryIndexPattern?: DataView) => void; - setBoundaryGeoField: (boundaryGeoField?: string) => void; - setBoundaryNameField: (boundaryNameField?: string) => void; - data: DataPublicPluginStart; - unifiedSearch: UnifiedSearchPublicPluginStart; -} - -interface KibanaDeps { - http: HttpSetup; -} - -export const BoundaryIndexExpression: FunctionComponent = ({ - ruleParams, - errors, - boundaryIndexPattern, - boundaryNameField, - setBoundaryIndexPattern, - setBoundaryGeoField, - setBoundaryNameField, - data, - unifiedSearch, -}) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - const BOUNDARY_NAME_ENTITY_TYPES = ['string', 'number', 'ip']; - const { http } = useKibana().services; - const IndexPatternSelect = (unifiedSearch.ui && unifiedSearch.ui.IndexPatternSelect) || null; - const { boundaryGeoField } = ruleParams; - // eslint-disable-next-line react-hooks/exhaustive-deps - const nothingSelected: DataViewField = { - name: '', - type: 'string', - } as DataViewField; - - const usePrevious = (value: T): T | undefined => { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; - }; - - const oldIndexPattern = usePrevious(boundaryIndexPattern); - const fields = useRef<{ - geoFields: DataViewField[]; - boundaryNameFields: DataViewField[]; - }>({ - geoFields: [], - boundaryNameFields: [], - }); - useEffect(() => { - if (oldIndexPattern !== boundaryIndexPattern) { - fields.current.geoFields = - (boundaryIndexPattern.fields && - boundaryIndexPattern.fields.length && - boundaryIndexPattern.fields.filter((field: DataViewField) => - ES_GEO_SHAPE_TYPES.includes(field.type) - )) || - []; - if (fields.current.geoFields.length) { - setBoundaryGeoField(fields.current.geoFields[0].name); - } - - fields.current.boundaryNameFields = [ - ...(boundaryIndexPattern.fields ?? []).filter((field: DataViewField) => { - return ( - BOUNDARY_NAME_ENTITY_TYPES.includes(field.type) && - !field.name.startsWith('_') && - !field.name.endsWith('keyword') - ); - }), - nothingSelected, - ]; - if (fields.current.boundaryNameFields.length) { - setBoundaryNameField(fields.current.boundaryNameFields[0].name); - } - } - }, [ - BOUNDARY_NAME_ENTITY_TYPES, - boundaryIndexPattern, - nothingSelected, - oldIndexPattern, - setBoundaryGeoField, - setBoundaryNameField, - ]); - - const indexPopover = ( - - - { - if (!_indexPattern) { - return; - } - setBoundaryIndexPattern(_indexPattern); - }} - value={boundaryIndexPattern.id} - IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={data.indexPatterns} - http={http} - includedGeoTypes={ES_GEO_SHAPE_TYPES} - /> - - - - - - { - setBoundaryNameField(name === nothingSelected.name ? undefined : name); - }} - fields={fields.current.boundaryNameFields} - /> - - - ); - - return ( - - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_by_expression.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_by_expression.test.tsx deleted file mode 100644 index 7d1ef64448311..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_by_expression.test.tsx +++ /dev/null @@ -1,95 +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 { mount } from 'enzyme'; -import { EntityByExpression, getValidIndexPatternFields } from './entity_by_expression'; -import { DataViewField } from '@kbn/data-views-plugin/public'; - -const defaultProps = { - errors: { - index: [], - indexId: [], - geoField: [], - entity: [], - dateField: [], - boundaryType: [], - boundaryIndexTitle: [], - boundaryIndexId: [], - boundaryGeoField: [], - name: ['Name is required.'], - interval: [], - alertTypeId: [], - actionConnectors: [], - }, - entity: 'FlightNum', - setAlertParamsEntity: (arg: string) => {}, - indexFields: [ - { - count: 0, - name: 'DestLocation', - type: 'geo_point', - esTypes: ['geo_point'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - count: 0, - name: 'FlightNum', - type: 'string', - esTypes: ['keyword'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - count: 0, - name: 'OriginLocation', - type: 'geo_point', - esTypes: ['geo_point'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - { - count: 0, - name: 'timestamp', - type: 'date', - esTypes: ['date'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - ] as DataViewField[], - isInvalid: false, -}; - -test('should render entity by expression with aggregatable field options for entity', async () => { - const component = mount(); - expect(component.render()).toMatchSnapshot(); -}); -// - -test('should only use valid index fields', async () => { - // Only the string index field should match - const indexFields = getValidIndexPatternFields(defaultProps.indexFields); - expect(indexFields.length).toEqual(1); - - // Set all agg fields to false, invalidating them for use - const invalidIndexFields = defaultProps.indexFields.map((field) => ({ - ...field, - aggregatable: false, - })); - - const noIndexFields = getValidIndexPatternFields(invalidIndexFields as DataViewField[]); - expect(noIndexFields.length).toEqual(0); -}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_by_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_by_expression.tsx deleted file mode 100644 index b0b02fe27f8ef..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_by_expression.tsx +++ /dev/null @@ -1,92 +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, { FunctionComponent, useEffect, useRef } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; -import { DataViewField } from '@kbn/data-views-plugin/public'; -import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; -import { SingleFieldSelect } from '../util_components/single_field_select'; -import { ExpressionWithPopover } from '../util_components/expression_with_popover'; - -interface Props { - errors: IErrorObject; - entity: string; - setAlertParamsEntity: (entity: string) => void; - indexFields: DataViewField[]; - isInvalid: boolean; -} - -const ENTITY_TYPES = ['string', 'number', 'ip']; -export function getValidIndexPatternFields(fields: DataViewField[]): DataViewField[] { - return fields.filter((field) => { - const isSpecifiedSupportedField = ENTITY_TYPES.includes(field.type); - const hasLeadingUnderscore = field.name.startsWith('_'); - const isAggregatable = !!field.aggregatable; - return isSpecifiedSupportedField && isAggregatable && !hasLeadingUnderscore; - }); -} - -export const EntityByExpression: FunctionComponent = ({ - errors, - entity, - setAlertParamsEntity, - indexFields, - isInvalid, -}) => { - const usePrevious = (value: T): T | undefined => { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; - }; - - const oldIndexFields = usePrevious(indexFields); - const fields = useRef<{ - indexFields: DataViewField[]; - }>({ - indexFields: [], - }); - useEffect(() => { - if (!_.isEqual(oldIndexFields, indexFields)) { - fields.current.indexFields = getValidIndexPatternFields(indexFields); - if (!entity && fields.current.indexFields.length) { - setAlertParamsEntity(fields.current.indexFields[0].name); - } - } - }, [indexFields, oldIndexFields, setAlertParamsEntity, entity]); - - const indexPopover = ( - - _entity && setAlertParamsEntity(_entity)} - fields={fields.current.indexFields} - /> - - ); - - return ( - - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_index_expression.tsx deleted file mode 100644 index fe75c7b22dcc2..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/expressions/entity_index_expression.tsx +++ /dev/null @@ -1,172 +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, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { HttpSetup } from '@kbn/core/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { - IErrorObject, - RuleTypeParamsExpressionProps, -} from '@kbn/triggers-actions-ui-plugin/public'; -import { DataViewField, DataView } from '@kbn/data-plugin/common'; -import { ES_GEO_FIELD_TYPES } from '../../types'; -import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; -import { SingleFieldSelect } from '../util_components/single_field_select'; -import { ExpressionWithPopover } from '../util_components/expression_with_popover'; - -interface Props { - dateField: string; - geoField: string; - errors: IErrorObject; - setAlertParamsDate: (date: string) => void; - setAlertParamsGeoField: (geoField: string) => void; - setRuleProperty: RuleTypeParamsExpressionProps['setRuleProperty']; - setIndexPattern: (indexPattern: DataView) => void; - indexPattern: DataView; - isInvalid: boolean; - data: DataPublicPluginStart; - unifiedSearch: UnifiedSearchPublicPluginStart; -} - -interface KibanaDeps { - http: HttpSetup; -} - -export const EntityIndexExpression: FunctionComponent = ({ - setAlertParamsDate, - setAlertParamsGeoField, - errors, - setIndexPattern, - indexPattern, - isInvalid, - dateField: timeField, - geoField, - data, - unifiedSearch, -}) => { - const { http } = useKibana().services; - const IndexPatternSelect = (unifiedSearch.ui && unifiedSearch.ui.IndexPatternSelect) || null; - - const usePrevious = (value: T): T | undefined => { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; - }; - - const oldIndexPattern = usePrevious(indexPattern); - const fields = useRef<{ - dateFields: DataViewField[]; - geoFields: DataViewField[]; - }>({ - dateFields: [], - geoFields: [], - }); - useEffect(() => { - if (oldIndexPattern !== indexPattern) { - fields.current.geoFields = - (indexPattern.fields && - indexPattern.fields.length && - indexPattern.fields.filter((field: DataViewField) => - ES_GEO_FIELD_TYPES.includes(field.type) - )) || - []; - if (fields.current.geoFields.length) { - setAlertParamsGeoField(fields.current.geoFields[0].name); - } - - fields.current.dateFields = - (indexPattern.fields && - indexPattern.fields.length && - indexPattern.fields.filter((field: DataViewField) => field.type === 'date')) || - []; - if (fields.current.dateFields.length) { - setAlertParamsDate(fields.current.dateFields[0].name); - } - } - }, [indexPattern, oldIndexPattern, setAlertParamsDate, setAlertParamsGeoField]); - - const indexPopover = ( - - - { - // reset time field and expression fields if indices are deleted - if (!_indexPattern) { - return; - } - setIndexPattern(_indexPattern); - }} - value={indexPattern.id} - IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={data.indexPatterns} - http={http} - includedGeoTypes={ES_GEO_FIELD_TYPES} - /> - - - } - > - - _timeField && setAlertParamsDate(_timeField) - } - fields={fields.current.dateFields} - /> - - - - _geoField && setAlertParamsGeoField(_geoField) - } - fields={fields.current.geoFields} - /> - - - ); - - return ( - - ); -}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/geo_containment_alert_type_expression.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/geo_containment_alert_type_expression.test.tsx deleted file mode 100644 index 5ef79b7379035..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/geo_containment_alert_type_expression.test.tsx +++ /dev/null @@ -1,88 +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 { shallow } from 'enzyme'; -import { EntityIndexExpression } from './expressions/entity_index_expression'; -import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; -import { DataView } from '@kbn/data-plugin/common'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; - -const dataStartMock = dataPluginMock.createStartContract(); -const unifiedSearchStartMock = unifiedSearchPluginMock.createStartContract(); - -const alertParams = { - index: '', - indexId: '', - geoField: '', - entity: '', - dateField: '', - boundaryType: '', - boundaryIndexTitle: '', - boundaryIndexId: '', - boundaryGeoField: '', -}; - -test('should render EntityIndexExpression', async () => { - const component = shallow( - {}} - setAlertParamsGeoField={() => {}} - setRuleProperty={() => {}} - setIndexPattern={() => {}} - indexPattern={'' as unknown as DataView} - isInvalid={false} - data={dataStartMock} - unifiedSearch={unifiedSearchStartMock} - /> - ); - - expect(component).toMatchSnapshot(); -}); - -test('should render EntityIndexExpression w/ invalid flag if invalid', async () => { - const component = shallow( - {}} - setAlertParamsGeoField={() => {}} - setRuleProperty={() => {}} - setIndexPattern={() => {}} - indexPattern={'' as unknown as DataView} - isInvalid={true} - data={dataStartMock} - unifiedSearch={unifiedSearchStartMock} - /> - ); - - expect(component).toMatchSnapshot(); -}); - -test('should render BoundaryIndexExpression', async () => { - const component = shallow( - {}} - setBoundaryGeoField={() => {}} - setBoundaryNameField={() => {}} - boundaryNameField={'testNameField'} - data={dataStartMock} - unifiedSearch={unifiedSearchStartMock} - /> - ); - - expect(component).toMatchSnapshot(); -}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/index.tsx deleted file mode 100644 index d49ee6c15b83a..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/index.tsx +++ /dev/null @@ -1,298 +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, { Fragment, useEffect, useState } from 'react'; -import { EuiCallOut, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { fromKueryExpression, luceneStringToDsl } from '@kbn/es-query'; -import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; -import type { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import type { Query } from '@kbn/es-query'; -import { QueryStringInput } from '@kbn/unified-search-plugin/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { HttpSetup } from '@kbn/core-http-browser'; -import type { DocLinksStart } from '@kbn/core-doc-links-browser'; -import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; -import type { CoreStart } from '@kbn/core/public'; -import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; -import { STACK_ALERTS_FEATURE_ID } from '../../../../common/constants'; -import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { EntityByExpression } from './expressions/entity_by_expression'; -import { EntityIndexExpression } from './expressions/entity_index_expression'; -import type { GeoContainmentAlertParams } from '../types'; - -const DEFAULT_VALUES = { - TRACKING_EVENT: '', - ENTITY: '', - INDEX: '', - INDEX_ID: '', - DATE_FIELD: '', - BOUNDARY_TYPE: 'entireIndex', // Only one supported currently. Will eventually be more - GEO_FIELD: '', - BOUNDARY_INDEX: '', - BOUNDARY_INDEX_ID: '', - BOUNDARY_GEO_FIELD: '', - BOUNDARY_NAME_FIELD: '', - DELAY_OFFSET_WITH_UNITS: '0m', -}; - -interface KibanaDeps { - http: HttpSetup; - docLinks: DocLinksStart; - dataViews: DataViewsPublicPluginStart; - uiSettings: IUiSettingsClient; - notifications: CoreStart['notifications']; - storage: IStorageWrapper; - usageCollection: UsageCollectionStart; -} - -function validateQuery(query: Query) { - try { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - query.language === 'kuery' ? fromKueryExpression(query.query) : luceneStringToDsl(query.query); - } catch (err) { - return false; - } - return true; -} - -export const GeoContainmentAlertTypeExpression: React.FunctionComponent< - RuleTypeParamsExpressionProps -> = ({ ruleParams, ruleInterval, setRuleParams, setRuleProperty, errors, data, unifiedSearch }) => { - const { - index, - indexId, - indexQuery, - geoField, - entity, - dateField, - boundaryType, - boundaryIndexTitle, - boundaryIndexId, - boundaryIndexQuery, - boundaryGeoField, - boundaryNameField, - } = ruleParams; - - const { http, docLinks, uiSettings, notifications, storage, usageCollection, dataViews } = - useKibana().services; - - const [indexPattern, _setIndexPattern] = useState({ - id: '', - title: '', - } as DataView); - const setIndexPattern = (_indexPattern?: DataView) => { - if (_indexPattern) { - _setIndexPattern(_indexPattern); - if (_indexPattern.title) { - setRuleParams('index', _indexPattern.title); - } - if (_indexPattern.id) { - setRuleParams('indexId', _indexPattern.id); - } - } - }; - const [indexQueryInput, setIndexQueryInput] = useState( - indexQuery || { - query: '', - language: 'kuery', - } - ); - const [boundaryIndexPattern, _setBoundaryIndexPattern] = useState({ - id: '', - title: '', - } as DataView); - const setBoundaryIndexPattern = (_indexPattern?: DataView) => { - if (_indexPattern) { - _setBoundaryIndexPattern(_indexPattern); - if (_indexPattern.title) { - setRuleParams('boundaryIndexTitle', _indexPattern.title); - } - if (_indexPattern.id) { - setRuleParams('boundaryIndexId', _indexPattern.id); - } - } - }; - const [boundaryIndexQueryInput, setBoundaryIndexQueryInput] = useState( - boundaryIndexQuery || { - query: '', - language: 'kuery', - } - ); - - const hasExpressionErrors = false; - const expressionErrorMessage = i18n.translate( - 'xpack.stackAlerts.geoContainment.fixErrorInExpressionBelowValidationMessage', - { - defaultMessage: 'Expression contains errors.', - } - ); - - useEffect(() => { - const initToDefaultParams = async () => { - setRuleProperty('params', { - ...ruleParams, - index: index ?? DEFAULT_VALUES.INDEX, - indexId: indexId ?? DEFAULT_VALUES.INDEX_ID, - entity: entity ?? DEFAULT_VALUES.ENTITY, - dateField: dateField ?? DEFAULT_VALUES.DATE_FIELD, - boundaryType: boundaryType ?? DEFAULT_VALUES.BOUNDARY_TYPE, - geoField: geoField ?? DEFAULT_VALUES.GEO_FIELD, - boundaryIndexTitle: boundaryIndexTitle ?? DEFAULT_VALUES.BOUNDARY_INDEX, - boundaryIndexId: boundaryIndexId ?? DEFAULT_VALUES.BOUNDARY_INDEX_ID, - boundaryGeoField: boundaryGeoField ?? DEFAULT_VALUES.BOUNDARY_GEO_FIELD, - boundaryNameField: boundaryNameField ?? DEFAULT_VALUES.BOUNDARY_NAME_FIELD, - }); - if (!data.indexPatterns) { - return; - } - if (indexId) { - const _indexPattern = await data.indexPatterns.get(indexId); - setIndexPattern(_indexPattern); - } - if (boundaryIndexId) { - const _boundaryIndexPattern = await data.indexPatterns.get(boundaryIndexId); - setBoundaryIndexPattern(_boundaryIndexPattern); - } - }; - initToDefaultParams(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - {hasExpressionErrors ? ( - - - - - - ) : null} - - -
- -
-
- - setRuleParams('dateField', _date)} - setAlertParamsGeoField={(_geoField) => setRuleParams('geoField', _geoField)} - setRuleProperty={setRuleProperty} - setIndexPattern={setIndexPattern} - indexPattern={indexPattern} - isInvalid={!indexId || !dateField || !geoField} - data={data} - unifiedSearch={unifiedSearch} - /> - setRuleParams('entity', entityName)} - indexFields={indexPattern.fields} - isInvalid={indexId && dateField && geoField ? !entity : false} - /> - - - { - if (query.language) { - if (validateQuery(query)) { - setRuleParams('indexQuery', query); - } - setIndexQueryInput(query); - } - }} - appName={STACK_ALERTS_FEATURE_ID} - deps={{ - unifiedSearch, - notifications, - http, - docLinks, - uiSettings, - data, - dataViews, - storage, - usageCollection, - }} - /> - - - -
- -
-
- - - _geoField && setRuleParams('boundaryGeoField', _geoField) - } - setBoundaryNameField={(_boundaryNameField: string | undefined) => - _boundaryNameField - ? setRuleParams('boundaryNameField', _boundaryNameField) - : setRuleParams('boundaryNameField', '') - } - boundaryNameField={boundaryNameField} - data={data} - unifiedSearch={unifiedSearch} - /> - - - { - if (query.language) { - if (validateQuery(query)) { - setRuleParams('boundaryIndexQuery', query); - } - setBoundaryIndexQueryInput(query); - } - }} - appName={STACK_ALERTS_FEATURE_ID} - deps={{ - unifiedSearch, - notifications, - http, - docLinks, - uiSettings, - data, - dataViews, - storage, - usageCollection, - }} - /> - - -
- ); -}; - -// eslint-disable-next-line import/no-default-export -export { GeoContainmentAlertTypeExpression as default }; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/__snapshots__/geo_index_pattern_select.test.tsx.snap b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/__snapshots__/geo_index_pattern_select.test.tsx.snap deleted file mode 100644 index f5f4c35f0edde..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/__snapshots__/geo_index_pattern_select.test.tsx.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render with error when data view does not have geo_point field 1`] = ` - - - - - -`; - -exports[`should render without error after mounting 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/expression_with_popover.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/expression_with_popover.tsx deleted file mode 100644 index 9509e3a534f44..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/expression_with_popover.tsx +++ /dev/null @@ -1,79 +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, { ReactNode, useState } from 'react'; -import { - EuiButtonIcon, - EuiExpression, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiPopoverTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export const ExpressionWithPopover: ({ - popoverContent, - expressionDescription, - defaultValue, - value, - isInvalid, -}: { - popoverContent: ReactNode; - expressionDescription: ReactNode; - defaultValue?: ReactNode; - value?: ReactNode; - isInvalid?: boolean; -}) => JSX.Element = ({ popoverContent, expressionDescription, defaultValue, value, isInvalid }) => { - const [popoverOpen, setPopoverOpen] = useState(false); - - return ( - setPopoverOpen(true)} - isInvalid={isInvalid} - /> - } - isOpen={popoverOpen} - closePopover={() => setPopoverOpen(false)} - ownFocus - anchorPosition="downLeft" - zIndex={8000} - display="block" - > -
- - - {expressionDescription} - - setPopoverOpen(false)} - /> - - - - {popoverContent} -
-
- ); -}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/geo_index_pattern_select.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/geo_index_pattern_select.test.tsx deleted file mode 100644 index 01abe69d5f8c1..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/geo_index_pattern_select.test.tsx +++ /dev/null @@ -1,73 +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 { shallow } from 'enzyme'; -import { GeoIndexPatternSelect } from './geo_index_pattern_select'; -import { DataViewsContract } from '@kbn/data-plugin/public'; -import { HttpSetup } from '@kbn/core/public'; - -class MockIndexPatternSelectComponent extends React.Component { - render() { - return 'MockIndexPatternSelectComponent'; - } -} - -function makeMockIndexPattern(id: string, fields: unknown) { - return { - id, - fields, - }; -} - -const mockIndexPatternService: DataViewsContract = { - get(id: string) { - if (id === 'foobar_with_geopoint') { - return makeMockIndexPattern(id, [{ type: 'geo_point' }]); - } else if (id === 'foobar_without_geopoint') { - return makeMockIndexPattern(id, [{ type: 'string' }]); - } - }, -} as unknown as DataViewsContract; - -test('should render without error after mounting', async () => { - const component = shallow( - {}} - value={'foobar_with_geopoint'} - includedGeoTypes={['geo_point']} - indexPatternService={mockIndexPatternService} - IndexPatternSelectComponent={MockIndexPatternSelectComponent} - /> - ); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); -}); - -test('should render with error when data view does not have geo_point field', async () => { - const component = shallow( - {}} - value={'foobar_without_geopoint'} - includedGeoTypes={['geo_point']} - indexPatternService={mockIndexPatternService} - IndexPatternSelectComponent={MockIndexPatternSelectComponent} - /> - ); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); -}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/geo_index_pattern_select.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/geo_index_pattern_select.tsx deleted file mode 100644 index b2a235feb1a80..0000000000000 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/geo_index_pattern_select.tsx +++ /dev/null @@ -1,178 +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, { Component } from 'react'; -import { EuiCallOut, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { DataViewsContract } from '@kbn/data-plugin/public'; -import { HttpSetup } from '@kbn/core/public'; -import { DataView } from '@kbn/data-plugin/common'; - -interface Props { - onChange: (indexPattern: DataView) => void; - value: string | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - IndexPatternSelectComponent: any; - indexPatternService: DataViewsContract | undefined; - http: HttpSetup; - includedGeoTypes: string[]; -} - -interface State { - doesIndexPatternHaveGeoField: boolean; - noIndexPatternsExist: boolean; -} - -export class GeoIndexPatternSelect extends Component { - private _isMounted: boolean = false; - - state = { - doesIndexPatternHaveGeoField: false, - noIndexPatternsExist: false, - }; - - componentWillUnmount() { - this._isMounted = false; - } - - componentDidMount() { - this._isMounted = true; - if (this.props.value) { - this._loadIndexPattern(this.props.value); - } - } - - _loadIndexPattern = async (indexPatternId: string) => { - if (!indexPatternId || indexPatternId.length === 0 || !this.props.indexPatternService) { - return; - } - - let indexPattern; - try { - indexPattern = await this.props.indexPatternService.get(indexPatternId); - } catch (err) { - return; - } - - if (!this._isMounted || indexPattern.id !== indexPatternId) { - return; - } - - this.setState({ - doesIndexPatternHaveGeoField: indexPattern.fields.some((field) => { - return this.props.includedGeoTypes.includes(field.type); - }), - }); - - return indexPattern; - }; - - _onIndexPatternSelect = async (indexPatternId: string) => { - const indexPattern = await this._loadIndexPattern(indexPatternId); - if (indexPattern) { - this.props.onChange(indexPattern); - } - }; - - _onNoIndexPatterns = () => { - this.setState({ noIndexPatternsExist: true }); - }; - - _renderNoIndexPatternWarning() { - if (!this.state.noIndexPatternsExist) { - return null; - } - - return ( - <> - -

- - - - -

-

- - - - -

-
- - - ); - } - - render() { - const IndexPatternSelectComponent = this.props.IndexPatternSelectComponent; - const isIndexPatternInvalid = !!this.props.value && !this.state.doesIndexPatternHaveGeoField; - const error = isIndexPatternInvalid - ? i18n.translate('xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message', { - defaultMessage: - 'Data view does not contain any allowed geospatial fields. Must have one of type {geoFields}.', - values: { - geoFields: this.props.includedGeoTypes.join(', '), - }, - }) - : ''; - - return ( - <> - {this._renderNoIndexPatternWarning()} - - - {IndexPatternSelectComponent ? ( - - ) : ( -
- )} - - - ); - } -} diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.test.tsx new file mode 100644 index 0000000000000..971d8e24df7d9 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { BoundaryForm } from './boundary_form'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +jest.mock('./query_input', () => { + return { + QueryInput: () =>
mock query input
, + }; +}); + +test('should not call prop callbacks on render', async () => { + const DATA_VIEW_TITLE = 'my-boundaries*'; + const DATA_VIEW_ID = '1234'; + const mockDataView = { + id: DATA_VIEW_ID, + fields: [ + { + name: 'location', + type: 'geo_shape', + }, + ], + title: DATA_VIEW_TITLE, + }; + const props = { + data: { + indexPatterns: { + get: async () => mockDataView, + }, + } as unknown as DataPublicPluginStart, + getValidationError: () => null, + ruleParams: { + boundaryIndexTitle: DATA_VIEW_TITLE, + boundaryIndexId: DATA_VIEW_ID, + boundaryGeoField: 'location', + boundaryNameField: 'name', + boundaryIndexQuery: { + query: 'population > 1000', + language: 'kuery', + }, + } as unknown as GeoContainmentAlertParams, + setDataViewId: jest.fn(), + setDataViewTitle: jest.fn(), + setGeoField: jest.fn(), + setNameField: jest.fn(), + setQuery: jest.fn(), + unifiedSearch: { + ui: { + IndexPatternSelect: () => { + return '
mock IndexPatternSelect
'; + }, + }, + } as unknown as UnifiedSearchPublicPluginStart, + }; + + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + // Assert that geospatial dataView fields are loaded + // to ensure test is properly awaiting async useEffect + let geoFieldsLoaded = false; + wrapper.findWhere((n) => { + if ( + n.name() === 'SingleFieldSelect' && + n.props().value === 'location' && + n.props().fields.length === 1 + ) { + geoFieldsLoaded = true; + } + return false; + }); + expect(geoFieldsLoaded).toBe(true); + + expect(props.setDataViewId).not.toHaveBeenCalled(); + expect(props.setDataViewTitle).not.toHaveBeenCalled(); + expect(props.setGeoField).not.toHaveBeenCalled(); + expect(props.setNameField).not.toHaveBeenCalled(); + expect(props.setQuery).not.toHaveBeenCalled(); +}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.tsx new file mode 100644 index 0000000000000..9674ee3a7fb02 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/boundary_form.tsx @@ -0,0 +1,233 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFormRow, EuiPanel, EuiSkeletonText, EuiSpacer, EuiTitle } from '@elastic/eui'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { Query } from '@kbn/es-query'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import { DataViewSelect } from './data_view_select'; +import { SingleFieldSelect } from './single_field_select'; +import { QueryInput } from './query_input'; + +export const BOUNDARY_GEO_FIELD_TYPES = ['geo_shape']; + +function getGeoFields(fields: DataViewField[]) { + return fields.filter((field: DataViewField) => BOUNDARY_GEO_FIELD_TYPES.includes(field.type)); +} + +function getNameFields(fields: DataViewField[]) { + return fields.filter( + (field: DataViewField) => + ['string', 'number', 'ip'].includes(field.type) && + !field.name.startsWith('_') && + !field.name.endsWith('keyword') + ); +} + +interface Props { + data: DataPublicPluginStart; + getValidationError: (key: string) => string | null; + ruleParams: GeoContainmentAlertParams; + setDataViewId: (id: string) => void; + setDataViewTitle: (title: string) => void; + setGeoField: (fieldName: string) => void; + setNameField: (fieldName: string | undefined) => void; + setQuery: (query: Query) => void; + unifiedSearch: UnifiedSearchPublicPluginStart; +} + +export const BoundaryForm = (props: Props) => { + const [isLoading, setIsLoading] = useState(false); + const [dataView, setDataView] = useState(); + const [dataViewNotFound, setDataViewNotFound] = useState(false); + const [geoFields, setGeoFields] = useState([]); + const [nameFields, setNameFields] = useState([]); + + useEffect(() => { + if (!props.ruleParams.boundaryIndexId || props.ruleParams.boundaryIndexId === dataView?.id) { + return; + } + + let ignore = false; + setIsLoading(true); + setDataViewNotFound(false); + props.data.indexPatterns + .get(props.ruleParams.boundaryIndexId) + .then((nextDataView) => { + if (!ignore) { + setDataView(nextDataView); + setGeoFields(getGeoFields(nextDataView.fields)); + setNameFields(getNameFields(nextDataView.fields)); + setIsLoading(false); + } + }) + .catch(() => { + if (!ignore) { + setDataViewNotFound(true); + setIsLoading(false); + } + }); + + return () => { + ignore = true; + }; + }, [props.ruleParams.boundaryIndexId, dataView?.id, props.data.indexPatterns]); + + function getDataViewError() { + const validationError = props.getValidationError('boundaryIndexTitle'); + if (validationError) { + return validationError; + } + + if (dataView && geoFields.length === 0) { + return i18n.translate('xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message', { + defaultMessage: + 'Data view does not contain geospatial fields. Must have one of type: {geoFieldTypes}.', + values: { + geoFieldTypes: BOUNDARY_GEO_FIELD_TYPES.join(', '), + }, + }); + } + + if (dataViewNotFound) { + return i18n.translate('xpack.stackAlerts.geoContainment.dataViewNotFound', { + defaultMessage: `Unable to find data view '{id}'`, + values: { id: props.ruleParams.indexId }, + }); + } + + return null; + } + + const dataViewError = getDataViewError(); + const geoFieldError = props.getValidationError('boundaryGeoField'); + + return ( + + +
+ +
+
+ + + + + + { + if (!nextDataView.id) { + return; + } + props.setDataViewId(nextDataView.id); + props.setDataViewTitle(nextDataView.title); + + const nextGeoFields = getGeoFields(nextDataView.fields); + if (nextGeoFields.length) { + props.setGeoField(nextGeoFields[0].name); + } else if ('boundaryGeoField' in props.ruleParams) { + props.setGeoField(''); + } + + // do not attempt to auto select name field + // its optional plus there can be many matches so auto selecting the correct field is improbable + if ('boundaryNameField' in props.ruleParams) { + props.setNameField(undefined); + } + }} + unifiedSearch={props.unifiedSearch} + /> + + + {props.ruleParams.boundaryIndexId && ( + <> + + { + if (fieldName) { + props.setGeoField(fieldName); + } + }} + fields={geoFields} + /> + + + + { + if (fieldName) { + props.setNameField(fieldName); + } + }} + fields={nameFields} + /> + + + + { + props.setQuery(query); + }} + query={props.ruleParams.boundaryIndexQuery} + /> + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/data_view_select.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/data_view_select.tsx new file mode 100644 index 0000000000000..5b162a1fb5ff0 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/data_view_select.tsx @@ -0,0 +1,58 @@ +/* + * 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, { useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { i18n } from '@kbn/i18n'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; + +interface Props { + data: DataPublicPluginStart; + dataViewId?: string; + isInvalid: boolean; + onChange: (dataview: DataView) => void; + unifiedSearch: UnifiedSearchPublicPluginStart; +} + +export const DataViewSelect = (props: Props) => { + const [isLoading, setIsLoading] = useState(false); + const isMounted = useMountedState(); + + return ( + { + if (!dataViewId) { + return; + } + try { + setIsLoading(true); + const dataView = await props.data.indexPatterns.get(dataViewId); + if (isMounted()) { + props.onChange(dataView); + setIsLoading(false); + } + } catch (error) { + // ignore indexPatterns.get error, + // if data view does not exist, select will not update rule params + if (isMounted()) { + setIsLoading(false); + } + } + }} + placeholder={i18n.translate('xpack.stackAlerts.geoContainment.dataViewSelectPlaceholder', { + defaultMessage: 'Select data view', + })} + /> + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.test.tsx new file mode 100644 index 0000000000000..da0f52897dd42 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { EntityForm } from './entity_form'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +jest.mock('./query_input', () => { + return { + QueryInput: () =>
mock query input
, + }; +}); + +test('should not call prop callbacks on render', async () => { + const DATA_VIEW_TITLE = 'my-entities*'; + const DATA_VIEW_ID = '1234'; + const mockDataView = { + id: DATA_VIEW_ID, + fields: [ + { + name: 'location', + type: 'geo_point', + }, + ], + title: DATA_VIEW_TITLE, + }; + const props = { + data: { + indexPatterns: { + get: async () => mockDataView, + }, + } as unknown as DataPublicPluginStart, + getValidationError: () => null, + ruleParams: { + index: DATA_VIEW_TITLE, + indexId: DATA_VIEW_ID, + geoField: 'location', + entity: 'entity_id', + dateField: 'time', + indexQuery: { + query: 'population > 1000', + language: 'kuery', + }, + } as unknown as GeoContainmentAlertParams, + setDataViewId: jest.fn(), + setDataViewTitle: jest.fn(), + setDateField: jest.fn(), + setEntityField: jest.fn(), + setGeoField: jest.fn(), + setQuery: jest.fn(), + unifiedSearch: { + ui: { + IndexPatternSelect: () => { + return '
mock IndexPatternSelect
'; + }, + }, + } as unknown as UnifiedSearchPublicPluginStart, + }; + + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + // Assert that geospatial dataView fields are loaded + // to ensure test is properly awaiting async useEffect + let geoFieldsLoaded = false; + wrapper.findWhere((n) => { + if ( + n.name() === 'SingleFieldSelect' && + n.props().value === 'location' && + n.props().fields.length === 1 + ) { + geoFieldsLoaded = true; + } + return false; + }); + expect(geoFieldsLoaded).toBe(true); + + expect(props.setDataViewId).not.toHaveBeenCalled(); + expect(props.setDataViewTitle).not.toHaveBeenCalled(); + expect(props.setDateField).not.toHaveBeenCalled(); + expect(props.setEntityField).not.toHaveBeenCalled(); + expect(props.setGeoField).not.toHaveBeenCalled(); + expect(props.setQuery).not.toHaveBeenCalled(); +}); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.tsx new file mode 100644 index 0000000000000..3923b494fa045 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/entity_form.tsx @@ -0,0 +1,277 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFormRow, EuiPanel, EuiSkeletonText, EuiSpacer, EuiTitle } from '@elastic/eui'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { Query } from '@kbn/es-query'; +import type { GeoContainmentAlertParams } from '../types'; +import { DataViewSelect } from './data_view_select'; +import { SingleFieldSelect } from './single_field_select'; +import { QueryInput } from './query_input'; + +export const ENTITY_GEO_FIELD_TYPES = ['geo_point', 'geo_shape']; + +function getDateFields(fields: DataViewField[]) { + return fields.filter((field: DataViewField) => field.type === 'date'); +} + +function getEntityFields(fields: DataViewField[]) { + return fields.filter( + (field: DataViewField) => + field.aggregatable && + ['string', 'number', 'ip'].includes(field.type) && + !field.name.startsWith('_') + ); +} + +function getGeoFields(fields: DataViewField[]) { + return fields.filter((field: DataViewField) => ENTITY_GEO_FIELD_TYPES.includes(field.type)); +} + +interface Props { + data: DataPublicPluginStart; + getValidationError: (key: string) => string | null; + ruleParams: GeoContainmentAlertParams; + setDataViewId: (id: string) => void; + setDataViewTitle: (title: string) => void; + setDateField: (fieldName: string) => void; + setEntityField: (fieldName: string) => void; + setGeoField: (fieldName: string) => void; + setQuery: (query: Query) => void; + unifiedSearch: UnifiedSearchPublicPluginStart; +} + +export const EntityForm = (props: Props) => { + const [isLoading, setIsLoading] = useState(false); + const [dataView, setDataView] = useState(); + const [dataViewNotFound, setDataViewNotFound] = useState(false); + const [dateFields, setDateFields] = useState([]); + const [entityFields, setEntityFields] = useState([]); + const [geoFields, setGeoFields] = useState([]); + + useEffect(() => { + if (!props.ruleParams.indexId || props.ruleParams.indexId === dataView?.id) { + return; + } + + let ignore = false; + setIsLoading(true); + setDataViewNotFound(false); + props.data.indexPatterns + .get(props.ruleParams.indexId) + .then((nextDataView) => { + if (!ignore) { + setDataView(nextDataView); + setDateFields(getDateFields(nextDataView.fields)); + setEntityFields(getEntityFields(nextDataView.fields)); + setGeoFields(getGeoFields(nextDataView.fields)); + setIsLoading(false); + } + }) + .catch(() => { + if (!ignore) { + setDataViewNotFound(true); + setIsLoading(false); + } + }); + + return () => { + ignore = true; + }; + }, [props.ruleParams.indexId, dataView?.id, props.data.indexPatterns]); + + function getDataViewError() { + const validationError = props.getValidationError('index'); + if (validationError) { + return validationError; + } + + if (dataView && dateFields.length === 0) { + return i18n.translate('xpack.stackAlerts.geoContainment.noDateFieldInIndexPattern.message', { + defaultMessage: 'Data view does not contain date fields.', + }); + } + + if (dataView && geoFields.length === 0) { + return i18n.translate('xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message', { + defaultMessage: + 'Data view does not contain geospatial fields. Must have one of type: {geoFieldTypes}.', + values: { + geoFieldTypes: ENTITY_GEO_FIELD_TYPES.join(', '), + }, + }); + } + + if (dataViewNotFound) { + return i18n.translate('xpack.stackAlerts.geoContainment.dataViewNotFound', { + defaultMessage: `Unable to find data view '{id}'`, + values: { id: props.ruleParams.indexId }, + }); + } + + return null; + } + + const dataViewError = getDataViewError(); + const dateFieldError = props.getValidationError('dateField'); + const geoFieldError = props.getValidationError('geoField'); + const entityFieldError = props.getValidationError('entity'); + + return ( + + +
+ +
+
+ + + + + + { + if (!nextDataView.id) { + return; + } + props.setDataViewId(nextDataView.id); + props.setDataViewTitle(nextDataView.title); + + const nextDateFields = getDateFields(nextDataView.fields); + if (nextDateFields.length) { + props.setDateField(nextDateFields[0].name); + } else if ('dateField' in props.ruleParams) { + props.setDateField(''); + } + + // do not attempt to auto select entity field + // there can be many matches so auto selecting the correct field is improbable + if ('entity' in props.ruleParams) { + props.setEntityField(''); + } + + const nextGeoFields = getGeoFields(nextDataView.fields); + if (nextGeoFields.length) { + props.setGeoField(nextGeoFields[0].name); + } else if ('geoField' in props.ruleParams) { + props.setGeoField(''); + } + }} + unifiedSearch={props.unifiedSearch} + /> + + + {props.ruleParams.indexId && ( + <> + + { + if (fieldName) { + props.setDateField(fieldName); + } + }} + fields={dateFields} + /> + + + + { + if (fieldName) { + props.setGeoField(fieldName); + } + }} + fields={geoFields} + /> + + + + { + if (fieldName) { + props.setEntityField(fieldName); + } + }} + fields={entityFields} + /> + + + + { + props.setQuery(query); + }} + query={props.ruleParams.indexQuery} + /> + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/index.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/index.ts new file mode 100644 index 0000000000000..dba9cea07ed8d --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { RuleForm } from './rule_form'; + +// eslint-disable-next-line import/no-default-export +export default RuleForm; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/query_input.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/query_input.tsx new file mode 100644 index 0000000000000..c07ce2ec8e090 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/query_input.tsx @@ -0,0 +1,98 @@ +/* + * 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, { useState } from 'react'; +import { QueryStringInput } from '@kbn/unified-search-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { Query } from '@kbn/es-query'; +import { fromKueryExpression, luceneStringToDsl } from '@kbn/es-query'; +import type { HttpSetup } from '@kbn/core-http-browser'; +import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; +import type { CoreStart } from '@kbn/core/public'; +import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; +import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { STACK_ALERTS_FEATURE_ID } from '../../../../common/constants'; + +function validateQuery(query: Query) { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + query.language === 'kuery' ? fromKueryExpression(query.query) : luceneStringToDsl(query.query); + } catch (err) { + return false; + } + return true; +} + +interface Props { + dataView?: DataView; + onChange: (query: Query) => void; + query?: Query; +} + +export const QueryInput = (props: Props) => { + const { + data, + dataViews, + docLinks, + http, + notifications, + storage, + uiSettings, + unifiedSearch, + usageCollection, + } = useKibana<{ + data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; + docLinks: DocLinksStart; + http: HttpSetup; + notifications: CoreStart['notifications']; + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; + unifiedSearch: UnifiedSearchPublicPluginStart; + usageCollection: UsageCollectionStart; + }>().services; + + const [localQuery, setLocalQuery] = useState( + props.query || { + query: '', + language: 'kuery', + } + ); + + return ( + { + if (query.language) { + setLocalQuery(query); + if (validateQuery(query)) { + props.onChange(query); + } + } + }} + appName={STACK_ALERTS_FEATURE_ID} + deps={{ + unifiedSearch, + notifications, + http, + docLinks, + uiSettings, + data, + dataViews, + storage, + usageCollection, + }} + /> + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/rule_form.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/rule_form.tsx new file mode 100644 index 0000000000000..52e6baae16536 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/rule_form.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiSpacer } from '@elastic/eui'; +import type { Query } from '@kbn/es-query'; +import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; +import type { GeoContainmentAlertParams } from '../types'; +import { BoundaryForm } from './boundary_form'; +import { EntityForm } from './entity_form'; + +export const RuleForm: React.FunctionComponent< + RuleTypeParamsExpressionProps +> = (props) => { + function getValidationError(key: string) { + return props.errors[key]?.length > 0 && key in props.ruleParams + ? (props.errors[key] as string[])[0] + : null; + } + + return ( + <> + props.setRuleParams('indexId', id)} + setDataViewTitle={(title: string) => props.setRuleParams('index', title)} + setDateField={(fieldName: string) => props.setRuleParams('dateField', fieldName)} + setEntityField={(fieldName: string) => props.setRuleParams('entity', fieldName)} + setGeoField={(fieldName: string) => props.setRuleParams('geoField', fieldName)} + setQuery={(query: Query) => props.setRuleParams('indexQuery', query)} + unifiedSearch={props.unifiedSearch} + /> + + + + { + props.setRuleParams('boundaryIndexId', id); + // TODO remove unused param 'boundaryType' + props.setRuleParams('boundaryType', 'entireIndex'); + }} + setDataViewTitle={(title: string) => props.setRuleParams('boundaryIndexTitle', title)} + setGeoField={(fieldName: string) => props.setRuleParams('boundaryGeoField', fieldName)} + setNameField={(fieldName: string | undefined) => + props.setRuleParams('boundaryNameField', fieldName) + } + setQuery={(query: Query) => props.setRuleParams('boundaryIndexQuery', query)} + unifiedSearch={props.unifiedSearch} + /> + + + + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/single_field_select.tsx b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/single_field_select.tsx similarity index 92% rename from x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/single_field_select.tsx rename to x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/single_field_select.tsx index 7783c943301f3..71ef2d301a9c7 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/query_builder/util_components/single_field_select.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/rule_form/single_field_select.tsx @@ -33,13 +33,14 @@ function fieldsToOptions(fields?: DataViewField[]): Array void; fields: DataViewField[]; } -export function SingleFieldSelect({ placeholder, value, onChange, fields }: Props) { +export function SingleFieldSelect({ isInvalid, placeholder, value, onChange, fields }: Props) { function renderOption( option: EuiComboBoxOptionOption, searchValue: string, @@ -71,6 +72,7 @@ export function SingleFieldSelect({ placeholder, value, onChange, fields }: Prop return ( ); } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts index b34dd9ec4f8d2..4eb717a23e437 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/types.ts @@ -22,6 +22,3 @@ export interface GeoContainmentAlertParams extends RuleTypeParams { indexQuery?: Query; boundaryIndexQuery?: Query; } - -export const ES_GEO_FIELD_TYPES = ['geo_point', 'geo_shape']; -export const ES_GEO_SHAPE_TYPES = ['geo_shape']; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts index 5312f5018d933..30aef903c6ef5 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.test.ts @@ -105,7 +105,7 @@ describe('expression params validation', () => { }; expect(validateExpression(initialParams).errors.boundaryIndexTitle.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.boundaryIndexTitle[0]).toBe( - 'Boundary data view title is required.' + 'Boundary data view is required.' ); }); diff --git a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts index 71fd748ea5df1..04fb961ac559b 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts +++ b/x-pack/plugins/stack_alerts/public/rule_types/geo_containment/validation.ts @@ -69,7 +69,7 @@ export const validateExpression = (alertParams: GeoContainmentAlertParams): Vali if (!boundaryIndexTitle) { errors.boundaryIndexTitle.push( i18n.translate('xpack.stackAlerts.geoContainment.error.requiredBoundaryIndexTitleText', { - defaultMessage: 'Boundary data view title is required.', + defaultMessage: 'Boundary data view is required.', }) ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 14688c6898e1f..c3d6b1732732f 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35656,7 +35656,6 @@ "xpack.stackAlerts.geoContainment.boundariesFetchError": "Impossible de récupérer les limites du suivi de l’endiguement, erreur : {error}", "xpack.stackAlerts.geoContainment.entityContainmentFetchError": "Impossible de récupérer l’endiguement des entités, erreur : {error}", "xpack.stackAlerts.geoContainment.noBoundariesError": "Aucune limite de suivi de l’endiguement trouvée. Assurez-vous que l’index \"{index}\" a des documents.", - "xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message": "La vue de données ne contient aucun champ géospatial autorisé. Il doit en contenir un de type {geoFields}.", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "le groupe {group} de l'alerte {name} a atteint le seuil", "xpack.stackAlerts.indexThreshold.alertTypeRecoveryContextSubjectTitle": "groupe {group} de l'alerte {name} récupéré", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "thresholdComparator spécifié non valide : {comparator}", @@ -35747,12 +35746,8 @@ "xpack.stackAlerts.geoContainment.actionVariableContextFromEntityDocumentIdLabel": "ID du document de l'entité contenue", "xpack.stackAlerts.geoContainment.actionVariableContextFromEntityLocationLabel": "Emplacement de l'entité", "xpack.stackAlerts.geoContainment.alertTypeTitle": "Suivi de l'endiguement", - "xpack.stackAlerts.geoContainment.boundaryNameSelect": "Sélectionner un nom de limite", "xpack.stackAlerts.geoContainment.boundaryNameSelectLabel": "Nom de limite lisible par l'utilisateur (facultatif)", "xpack.stackAlerts.geoContainment.descriptionText": "Alerte lorsqu'une entité est contenue dans une limite géographique.", - "xpack.stackAlerts.geoContainment.entityByLabel": "par", - "xpack.stackAlerts.geoContainment.entityIndexLabel": "index", - "xpack.stackAlerts.geoContainment.entityIndexSelect": "Sélectionner une vue de données et un champ de point géographique", "xpack.stackAlerts.geoContainment.error.requiredBoundaryGeoFieldText": "Le champ de limite géographique est requis.", "xpack.stackAlerts.geoContainment.error.requiredBoundaryIndexTitleText": "Le titre de la vue de données de limite est requis.", "xpack.stackAlerts.geoContainment.error.requiredBoundaryTypeText": "Le type de limite est requis.", @@ -35760,25 +35755,12 @@ "xpack.stackAlerts.geoContainment.error.requiredEntityText": "L'entité est requise.", "xpack.stackAlerts.geoContainment.error.requiredGeoFieldText": "Le champ géographique est requis.", "xpack.stackAlerts.geoContainment.error.requiredIndexTitleText": "La vue de données est requise.", - "xpack.stackAlerts.geoContainment.fixErrorInExpressionBelowValidationMessage": "L'expression contient des erreurs.", "xpack.stackAlerts.geoContainment.geofieldLabel": "Champ géospatial", - "xpack.stackAlerts.geoContainment.indexLabel": "index", - "xpack.stackAlerts.geoContainment.indexPatternSelectLabel": "Vue de données", - "xpack.stackAlerts.geoContainment.indexPatternSelectPlaceholder": "Sélectionner la vue de données", - "xpack.stackAlerts.geoContainment.noIndexPattern.doThisLinkTextDescription": "Créez une vue de données.", - "xpack.stackAlerts.geoContainment.noIndexPattern.doThisPrefixDescription": "Vous devrez ", - "xpack.stackAlerts.geoContainment.noIndexPattern.getStartedLinkText": "Commencez avec des échantillons d'ensembles de données.", - "xpack.stackAlerts.geoContainment.noIndexPattern.hintDescription": "Vous n'avez aucune donnée ? ", - "xpack.stackAlerts.geoContainment.noIndexPattern.messageTitle": "Aucune vue de données n'a été trouvée", "xpack.stackAlerts.geoContainment.notGeoContained": "Plus contenu", - "xpack.stackAlerts.geoContainment.selectBoundaryIndex": "Sélectionner une limite", - "xpack.stackAlerts.geoContainment.selectEntity": "Sélectionner une entité", "xpack.stackAlerts.geoContainment.selectGeoLabel": "Sélectionner un champ géographique", - "xpack.stackAlerts.geoContainment.selectLabel": "Sélectionner un champ géographique", "xpack.stackAlerts.geoContainment.selectTimeLabel": "Sélectionner un champ temporel", "xpack.stackAlerts.geoContainment.timeFieldLabel": "Champ temporel", "xpack.stackAlerts.geoContainment.topHitsSplitFieldSelectPlaceholder": "Sélectionner un champ d'entité", - "xpack.stackAlerts.geoContainment.ui.expressionPopover.closePopoverLabel": "Fermer", "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "Seuil atteint", "xpack.stackAlerts.indexThreshold.actionVariableContextConditionsLabel": "Chaîne décrivant le comparateur de seuil et le seuil", "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "Date à laquelle l'alerte a dépassé le seuil.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6849a96b4bd4a..3936b78eb1a01 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35655,7 +35655,6 @@ "xpack.stackAlerts.geoContainment.boundariesFetchError": "追跡包含境界を取得できません。エラー:{error}", "xpack.stackAlerts.geoContainment.entityContainmentFetchError": "エンティティコンテインメントを取得できません。エラー:{error}", "xpack.stackAlerts.geoContainment.noBoundariesError": "追跡包含境界が見つかりません。インデックス\"{index}\"にドキュメントがあることを確認します。", - "xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message": "データビューには許可された地理空間フィールドが含まれていません。{geoFields}型のいずれかが必要です。", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "アラート{name}グループ{group}がしきい値を満たしました", "xpack.stackAlerts.indexThreshold.alertTypeRecoveryContextSubjectTitle": "アラート{name}グループ{group}が回復されました", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効なthresholdComparatorが指定されました:{comparator}", @@ -35746,12 +35745,8 @@ "xpack.stackAlerts.geoContainment.actionVariableContextFromEntityDocumentIdLabel": "含まれるエンティティドキュメントの ID", "xpack.stackAlerts.geoContainment.actionVariableContextFromEntityLocationLabel": "エンティティの場所", "xpack.stackAlerts.geoContainment.alertTypeTitle": "追跡包含", - "xpack.stackAlerts.geoContainment.boundaryNameSelect": "境界名を選択", "xpack.stackAlerts.geoContainment.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", "xpack.stackAlerts.geoContainment.descriptionText": "エンティティが地理的境界に含まれるときにアラートを発行します。", - "xpack.stackAlerts.geoContainment.entityByLabel": "グループ基準", - "xpack.stackAlerts.geoContainment.entityIndexLabel": "インデックス", - "xpack.stackAlerts.geoContainment.entityIndexSelect": "データビューと地理ポイントフィールドを選択", "xpack.stackAlerts.geoContainment.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", "xpack.stackAlerts.geoContainment.error.requiredBoundaryIndexTitleText": "境界データビュータイトルが必要です。", "xpack.stackAlerts.geoContainment.error.requiredBoundaryTypeText": "境界タイプは必須です。", @@ -35759,25 +35754,12 @@ "xpack.stackAlerts.geoContainment.error.requiredEntityText": "エンティティは必須です。", "xpack.stackAlerts.geoContainment.error.requiredGeoFieldText": "地理フィールドは必須です。", "xpack.stackAlerts.geoContainment.error.requiredIndexTitleText": "データビューが必要です。", - "xpack.stackAlerts.geoContainment.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", "xpack.stackAlerts.geoContainment.geofieldLabel": "地理空間フィールド", - "xpack.stackAlerts.geoContainment.indexLabel": "インデックス", - "xpack.stackAlerts.geoContainment.indexPatternSelectLabel": "データビュー", - "xpack.stackAlerts.geoContainment.indexPatternSelectPlaceholder": "データビューを選択", - "xpack.stackAlerts.geoContainment.noIndexPattern.doThisLinkTextDescription": "データビューを作成します。", - "xpack.stackAlerts.geoContainment.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", - "xpack.stackAlerts.geoContainment.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", - "xpack.stackAlerts.geoContainment.noIndexPattern.hintDescription": "データがない場合", - "xpack.stackAlerts.geoContainment.noIndexPattern.messageTitle": "データビューが見つかりませんでした", "xpack.stackAlerts.geoContainment.notGeoContained": "含まれていません", - "xpack.stackAlerts.geoContainment.selectBoundaryIndex": "境界を選択", - "xpack.stackAlerts.geoContainment.selectEntity": "エンティティを選択", "xpack.stackAlerts.geoContainment.selectGeoLabel": "ジオフィールドを選択", - "xpack.stackAlerts.geoContainment.selectLabel": "ジオフィールドを選択", "xpack.stackAlerts.geoContainment.selectTimeLabel": "時刻フィールドを選択", "xpack.stackAlerts.geoContainment.timeFieldLabel": "時間フィールド", "xpack.stackAlerts.geoContainment.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", - "xpack.stackAlerts.geoContainment.ui.expressionPopover.closePopoverLabel": "閉じる", "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "しきい値一致", "xpack.stackAlerts.indexThreshold.actionVariableContextConditionsLabel": "しきい値比較基準としきい値を説明する文字列", "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "アラートがしきい値を超えた日付。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9e1a41ddaea15..a75699cf01fca 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35649,7 +35649,6 @@ "xpack.stackAlerts.geoContainment.boundariesFetchError": "无法提取跟踪限制边界,错误:{error}", "xpack.stackAlerts.geoContainment.entityContainmentFetchError": "无法提取实体限制,错误:{error}", "xpack.stackAlerts.geoContainment.noBoundariesError": "找不到跟踪限制边界。确保索引“{index}”包含文档。", - "xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message": "数据视图不包含任何允许的地理空间字段。必须具有一个类型 {geoFields}。", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "告警 {name} 组 {group} 达到阈值", "xpack.stackAlerts.indexThreshold.alertTypeRecoveryContextSubjectTitle": "告警 {name} 组 {group} 已恢复", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", @@ -35740,12 +35739,8 @@ "xpack.stackAlerts.geoContainment.actionVariableContextFromEntityDocumentIdLabel": "所包含实体文档的 ID", "xpack.stackAlerts.geoContainment.actionVariableContextFromEntityLocationLabel": "实体的位置", "xpack.stackAlerts.geoContainment.alertTypeTitle": "跟踪限制", - "xpack.stackAlerts.geoContainment.boundaryNameSelect": "选择边界名称", "xpack.stackAlerts.geoContainment.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", "xpack.stackAlerts.geoContainment.descriptionText": "实体包含在地理边界内时告警。", - "xpack.stackAlerts.geoContainment.entityByLabel": "依据", - "xpack.stackAlerts.geoContainment.entityIndexLabel": "索引", - "xpack.stackAlerts.geoContainment.entityIndexSelect": "选择数据视图和地理点字段", "xpack.stackAlerts.geoContainment.error.requiredBoundaryGeoFieldText": "边界地理字段必填。", "xpack.stackAlerts.geoContainment.error.requiredBoundaryIndexTitleText": "边界数据视图标题必填。", "xpack.stackAlerts.geoContainment.error.requiredBoundaryTypeText": "“边界类型”必填。", @@ -35753,25 +35748,12 @@ "xpack.stackAlerts.geoContainment.error.requiredEntityText": "“实体”必填。", "xpack.stackAlerts.geoContainment.error.requiredGeoFieldText": "“地理”字段必填。", "xpack.stackAlerts.geoContainment.error.requiredIndexTitleText": "需要数据视图。", - "xpack.stackAlerts.geoContainment.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", "xpack.stackAlerts.geoContainment.geofieldLabel": "地理空间字段", - "xpack.stackAlerts.geoContainment.indexLabel": "索引", - "xpack.stackAlerts.geoContainment.indexPatternSelectLabel": "数据视图", - "xpack.stackAlerts.geoContainment.indexPatternSelectPlaceholder": "选择数据视图", - "xpack.stackAlerts.geoContainment.noIndexPattern.doThisLinkTextDescription": "创建数据视图。", - "xpack.stackAlerts.geoContainment.noIndexPattern.doThisPrefixDescription": "您将需要 ", - "xpack.stackAlerts.geoContainment.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", - "xpack.stackAlerts.geoContainment.noIndexPattern.hintDescription": "没有任何数据?", - "xpack.stackAlerts.geoContainment.noIndexPattern.messageTitle": "找不到任何数据视图", "xpack.stackAlerts.geoContainment.notGeoContained": "不再包含", - "xpack.stackAlerts.geoContainment.selectBoundaryIndex": "选择边界", - "xpack.stackAlerts.geoContainment.selectEntity": "选择实体", "xpack.stackAlerts.geoContainment.selectGeoLabel": "选择地理字段", - "xpack.stackAlerts.geoContainment.selectLabel": "选择地理字段", "xpack.stackAlerts.geoContainment.selectTimeLabel": "选择时间字段", "xpack.stackAlerts.geoContainment.timeFieldLabel": "时间字段", "xpack.stackAlerts.geoContainment.topHitsSplitFieldSelectPlaceholder": "选择实体字段", - "xpack.stackAlerts.geoContainment.ui.expressionPopover.closePopoverLabel": "关闭", "xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle": "已达到阈值", "xpack.stackAlerts.indexThreshold.actionVariableContextConditionsLabel": "描述阈值比较运算符和阈值的字符串", "xpack.stackAlerts.indexThreshold.actionVariableContextDateLabel": "告警超过阈值的日期。",