mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[maps][alerting] fix ES query rule boundary field changed when editing the rule (#165155)
Fixes https://github.com/elastic/kibana/issues/163959 While digging into the original issue, it was determined that the existing components were unsalvageable. Fixing all of the issues would have required more work than just starting over. Problems with original components include: 1) updating rule state on component load. This is the cause of the reported bug. 2) lack of loading state when performing async tasks, like loading data views. 3) not displaying validation errors. When users clicked "save" with missing configuration, no UI notifications were displayed 4) Heavy use of EuiExpression made it impossible to view all configuration in a single time Now, geo containment form: 1) Only updates rule state when users interact with inputs. 2) Displays loading state when performing async tasks, like loading data views. 3) Displays validation errors 4) Has a simpler UI that allows users to see all configuration information at the same time. <img width="300" alt="Screen Shot 2023-08-30 at 5 34 00 PM" src="65abfa5d
-6c8e-45a9-b69f-cc07f5be7184"> <img width="300" alt="Screen Shot 2023-08-30 at 5 34 48 PM" src="63b5af12
-7104-43ae-a836-0236cf9d1e98"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
633aebe5fc
commit
118ea87a08
31 changed files with 944 additions and 1722 deletions
Binary file not shown.
Before Width: | Height: | Size: 84 KiB |
|
@ -2,51 +2,25 @@
|
|||
[[geo-alerting]]
|
||||
== Tracking containment
|
||||
|
||||
<<maps, Maps>> 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 - <current interval>` 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%]
|
||||
|
|
|
@ -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<any>,
|
||||
'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'
|
||||
>,
|
||||
Omit<EuiComboBoxProps<any>, 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>,
|
||||
'placeholder'
|
||||
> & {
|
||||
onChange: (indexPatternId?: string) => void;
|
||||
|
@ -155,7 +152,7 @@ export default class IndexPatternSelect extends Component<IndexPatternSelectInte
|
|||
{...rest}
|
||||
placeholder={placeholder}
|
||||
singleSelection={true}
|
||||
isLoading={this.state.isLoading}
|
||||
isLoading={this.state.isLoading || this.props.isLoading}
|
||||
onSearchChange={this.fetchOptions}
|
||||
options={this.state.options}
|
||||
selectedOptions={this.state.selectedIndexPattern ? [this.state.selectedIndexPattern] : []}
|
||||
|
|
|
@ -15,11 +15,11 @@ export function getRuleType(): RuleTypeModel<GeoContainmentAlertParams> {
|
|||
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,
|
||||
};
|
||||
|
|
|
@ -1,278 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should render BoundaryIndexExpression 1`] = `
|
||||
<ExpressionWithPopover
|
||||
defaultValue="Select a data view and geo shape field"
|
||||
expressionDescription="index"
|
||||
popoverContent={
|
||||
<React.Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="geoIndexPatternSelect"
|
||||
labelType="label"
|
||||
>
|
||||
<GeoIndexPatternSelect
|
||||
IndexPatternSelectComponent={[MockFunction]}
|
||||
includedGeoTypes={
|
||||
Array [
|
||||
"geo_shape",
|
||||
]
|
||||
}
|
||||
indexPatternService={
|
||||
Object {
|
||||
"clearCache": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"createField": [MockFunction],
|
||||
"createFieldList": [MockFunction],
|
||||
"ensureDefaultDataView": [MockFunction],
|
||||
"ensureDefaultIndexPattern": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"getCanSaveSync": [MockFunction],
|
||||
"getDefaultDataView": [MockFunction],
|
||||
"getDefaultId": [MockFunction],
|
||||
"getFieldsForIndexPattern": [MockFunction],
|
||||
"getIdsWithTitle": [MockFunction],
|
||||
"hasData": Object {
|
||||
"hasDataView": [MockFunction],
|
||||
"hasESData": [MockFunction],
|
||||
"hasUserDataView": [MockFunction],
|
||||
},
|
||||
"make": [Function],
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="geoField"
|
||||
label="Geospatial field"
|
||||
labelType="label"
|
||||
>
|
||||
<SingleFieldSelect
|
||||
fields={Array []}
|
||||
onChange={[Function]}
|
||||
placeholder="Select geo field"
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="boundaryNameFieldSelect"
|
||||
label="Human-readable boundary name (optional)"
|
||||
labelType="label"
|
||||
>
|
||||
<SingleFieldSelect
|
||||
fields={Array []}
|
||||
onChange={[Function]}
|
||||
placeholder="Select boundary name"
|
||||
value="testNameField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`should render EntityIndexExpression 1`] = `
|
||||
<ExpressionWithPopover
|
||||
defaultValue="Select a data view and geospatial field"
|
||||
expressionDescription="index"
|
||||
isInvalid={false}
|
||||
popoverContent={
|
||||
<React.Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="geoIndexPatternSelect"
|
||||
labelType="label"
|
||||
>
|
||||
<GeoIndexPatternSelect
|
||||
IndexPatternSelectComponent={[MockFunction]}
|
||||
includedGeoTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
"geo_shape",
|
||||
]
|
||||
}
|
||||
indexPatternService={
|
||||
Object {
|
||||
"clearCache": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"createField": [MockFunction],
|
||||
"createFieldList": [MockFunction],
|
||||
"ensureDefaultDataView": [MockFunction],
|
||||
"ensureDefaultIndexPattern": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"getCanSaveSync": [MockFunction],
|
||||
"getDefaultDataView": [MockFunction],
|
||||
"getDefaultId": [MockFunction],
|
||||
"getFieldsForIndexPattern": [MockFunction],
|
||||
"getIdsWithTitle": [MockFunction],
|
||||
"hasData": Object {
|
||||
"hasDataView": [MockFunction],
|
||||
"hasESData": [MockFunction],
|
||||
"hasUserDataView": [MockFunction],
|
||||
},
|
||||
"make": [Function],
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="containmentTimeField"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Time field"
|
||||
id="xpack.stackAlerts.geoContainment.timeFieldLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<SingleFieldSelect
|
||||
fields={Array []}
|
||||
onChange={[Function]}
|
||||
placeholder="Select time field"
|
||||
value="testDateField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="geoField"
|
||||
label="Geospatial field"
|
||||
labelType="label"
|
||||
>
|
||||
<SingleFieldSelect
|
||||
fields={Array []}
|
||||
onChange={[Function]}
|
||||
placeholder="Select geo field"
|
||||
value="testGeoField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = `
|
||||
<ExpressionWithPopover
|
||||
defaultValue="Select a data view and geospatial field"
|
||||
expressionDescription="index"
|
||||
isInvalid={true}
|
||||
popoverContent={
|
||||
<React.Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="geoIndexPatternSelect"
|
||||
labelType="label"
|
||||
>
|
||||
<GeoIndexPatternSelect
|
||||
IndexPatternSelectComponent={[MockFunction]}
|
||||
includedGeoTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
"geo_shape",
|
||||
]
|
||||
}
|
||||
indexPatternService={
|
||||
Object {
|
||||
"clearCache": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"createField": [MockFunction],
|
||||
"createFieldList": [MockFunction],
|
||||
"ensureDefaultDataView": [MockFunction],
|
||||
"ensureDefaultIndexPattern": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"getCanSaveSync": [MockFunction],
|
||||
"getDefaultDataView": [MockFunction],
|
||||
"getDefaultId": [MockFunction],
|
||||
"getFieldsForIndexPattern": [MockFunction],
|
||||
"getIdsWithTitle": [MockFunction],
|
||||
"hasData": Object {
|
||||
"hasDataView": [MockFunction],
|
||||
"hasESData": [MockFunction],
|
||||
"hasUserDataView": [MockFunction],
|
||||
},
|
||||
"make": [Function],
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="containmentTimeField"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Time field"
|
||||
id="xpack.stackAlerts.geoContainment.timeFieldLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<SingleFieldSelect
|
||||
fields={Array []}
|
||||
onChange={[Function]}
|
||||
placeholder="Select time field"
|
||||
value="testDateField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="geoField"
|
||||
label="Geospatial field"
|
||||
labelType="label"
|
||||
>
|
||||
<SingleFieldSelect
|
||||
fields={Array []}
|
||||
onChange={[Function]}
|
||||
placeholder="Select geo field"
|
||||
value="testGeoField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -1,30 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should render entity by expression with aggregatable field options for entity 1`] = `
|
||||
<div
|
||||
class="euiPopover emotion-euiPopover"
|
||||
id="popoverForExpression"
|
||||
>
|
||||
<div
|
||||
class="euiPopover__anchor css-zih94u-render"
|
||||
>
|
||||
<button
|
||||
class="euiExpression emotion-euiExpression-isClickable-success-columns"
|
||||
data-test-subj="selectIndexExpression"
|
||||
>
|
||||
<span
|
||||
class="euiExpression__description emotion-euiExpression__description-success-isUppercase-columns"
|
||||
style="flex-basis: 20%;"
|
||||
>
|
||||
by
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="euiExpression__value emotion-euiExpression__value-columns"
|
||||
>
|
||||
FlightNum
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -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<Props> = ({
|
||||
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<KibanaDeps>().services;
|
||||
const IndexPatternSelect = (unifiedSearch.ui && unifiedSearch.ui.IndexPatternSelect) || null;
|
||||
const { boundaryGeoField } = ruleParams;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const nothingSelected: DataViewField = {
|
||||
name: '<nothing selected>',
|
||||
type: 'string',
|
||||
} as DataViewField;
|
||||
|
||||
const usePrevious = <T extends unknown>(value: T): T | undefined => {
|
||||
const ref = useRef<T>();
|
||||
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 = (
|
||||
<Fragment>
|
||||
<EuiFormRow id="geoIndexPatternSelect" fullWidth error={errors.index}>
|
||||
<GeoIndexPatternSelect
|
||||
onChange={(_indexPattern) => {
|
||||
if (!_indexPattern) {
|
||||
return;
|
||||
}
|
||||
setBoundaryIndexPattern(_indexPattern);
|
||||
}}
|
||||
value={boundaryIndexPattern.id}
|
||||
IndexPatternSelectComponent={IndexPatternSelect}
|
||||
indexPatternService={data.indexPatterns}
|
||||
http={http}
|
||||
includedGeoTypes={ES_GEO_SHAPE_TYPES}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="geoField"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.geofieldLabel', {
|
||||
defaultMessage: 'Geospatial field',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.stackAlerts.geoContainment.selectLabel', {
|
||||
defaultMessage: 'Select geo field',
|
||||
})}
|
||||
value={boundaryGeoField}
|
||||
onChange={setBoundaryGeoField}
|
||||
fields={fields.current.geoFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="boundaryNameFieldSelect"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.boundaryNameSelectLabel', {
|
||||
defaultMessage: 'Human-readable boundary name (optional)',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.stackAlerts.geoContainment.boundaryNameSelect', {
|
||||
defaultMessage: 'Select boundary name',
|
||||
})}
|
||||
value={boundaryNameField || null}
|
||||
onChange={(name) => {
|
||||
setBoundaryNameField(name === nothingSelected.name ? undefined : name);
|
||||
}}
|
||||
fields={fields.current.boundaryNameFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpressionWithPopover
|
||||
defaultValue={'Select a data view and geo shape field'}
|
||||
value={boundaryIndexPattern.title}
|
||||
popoverContent={indexPopover}
|
||||
expressionDescription={i18n.translate('xpack.stackAlerts.geoContainment.indexLabel', {
|
||||
defaultMessage: 'index',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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(<EntityByExpression {...defaultProps} />);
|
||||
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);
|
||||
});
|
|
@ -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<Props> = ({
|
||||
errors,
|
||||
entity,
|
||||
setAlertParamsEntity,
|
||||
indexFields,
|
||||
isInvalid,
|
||||
}) => {
|
||||
const usePrevious = <T extends unknown>(value: T): T | undefined => {
|
||||
const ref = useRef<T>();
|
||||
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 = (
|
||||
<EuiFormRow id="entitySelect" fullWidth error={errors.index}>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.topHitsSplitFieldSelectPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select entity field',
|
||||
}
|
||||
)}
|
||||
value={entity}
|
||||
onChange={(_entity) => _entity && setAlertParamsEntity(_entity)}
|
||||
fields={fields.current.indexFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpressionWithPopover
|
||||
isInvalid={isInvalid}
|
||||
value={entity}
|
||||
defaultValue={'Select entity field'}
|
||||
popoverContent={indexPopover}
|
||||
expressionDescription={i18n.translate('xpack.stackAlerts.geoContainment.entityByLabel', {
|
||||
defaultMessage: 'by',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<Props> = ({
|
||||
setAlertParamsDate,
|
||||
setAlertParamsGeoField,
|
||||
errors,
|
||||
setIndexPattern,
|
||||
indexPattern,
|
||||
isInvalid,
|
||||
dateField: timeField,
|
||||
geoField,
|
||||
data,
|
||||
unifiedSearch,
|
||||
}) => {
|
||||
const { http } = useKibana<KibanaDeps>().services;
|
||||
const IndexPatternSelect = (unifiedSearch.ui && unifiedSearch.ui.IndexPatternSelect) || null;
|
||||
|
||||
const usePrevious = <T extends unknown>(value: T): T | undefined => {
|
||||
const ref = useRef<T>();
|
||||
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 = (
|
||||
<Fragment>
|
||||
<EuiFormRow id="geoIndexPatternSelect" fullWidth error={errors.index}>
|
||||
<GeoIndexPatternSelect
|
||||
onChange={(_indexPattern) => {
|
||||
// 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}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="containmentTimeField"
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.timeFieldLabel"
|
||||
defaultMessage="Time field"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.stackAlerts.geoContainment.selectTimeLabel', {
|
||||
defaultMessage: 'Select time field',
|
||||
})}
|
||||
value={timeField}
|
||||
onChange={(_timeField: string | undefined) =>
|
||||
_timeField && setAlertParamsDate(_timeField)
|
||||
}
|
||||
fields={fields.current.dateFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
id="geoField"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.geofieldLabel', {
|
||||
defaultMessage: 'Geospatial field',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate('xpack.stackAlerts.geoContainment.selectGeoLabel', {
|
||||
defaultMessage: 'Select geo field',
|
||||
})}
|
||||
value={geoField}
|
||||
onChange={(_geoField: string | undefined) =>
|
||||
_geoField && setAlertParamsGeoField(_geoField)
|
||||
}
|
||||
fields={fields.current.geoFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpressionWithPopover
|
||||
isInvalid={isInvalid}
|
||||
value={indexPattern.title}
|
||||
defaultValue={i18n.translate('xpack.stackAlerts.geoContainment.entityIndexSelect', {
|
||||
defaultMessage: 'Select a data view and geospatial field',
|
||||
})}
|
||||
popoverContent={indexPopover}
|
||||
expressionDescription={i18n.translate('xpack.stackAlerts.geoContainment.entityIndexLabel', {
|
||||
defaultMessage: 'index',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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(
|
||||
<EntityIndexExpression
|
||||
dateField={'testDateField'}
|
||||
geoField={'testGeoField'}
|
||||
errors={{} as IErrorObject}
|
||||
setAlertParamsDate={() => {}}
|
||||
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(
|
||||
<EntityIndexExpression
|
||||
dateField={'testDateField'}
|
||||
geoField={'testGeoField'}
|
||||
errors={{} as IErrorObject}
|
||||
setAlertParamsDate={() => {}}
|
||||
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(
|
||||
<BoundaryIndexExpression
|
||||
ruleParams={alertParams}
|
||||
errors={{} as IErrorObject}
|
||||
boundaryIndexPattern={'' as unknown as DataView}
|
||||
setBoundaryIndexPattern={() => {}}
|
||||
setBoundaryGeoField={() => {}}
|
||||
setBoundaryNameField={() => {}}
|
||||
boundaryNameField={'testNameField'}
|
||||
data={dataStartMock}
|
||||
unifiedSearch={unifiedSearchStartMock}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
|
@ -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<GeoContainmentAlertParams>
|
||||
> = ({ 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<KibanaDeps>().services;
|
||||
|
||||
const [indexPattern, _setIndexPattern] = useState<DataView>({
|
||||
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<Query>(
|
||||
indexQuery || {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
}
|
||||
);
|
||||
const [boundaryIndexPattern, _setBoundaryIndexPattern] = useState<DataView>({
|
||||
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<Query>(
|
||||
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 (
|
||||
<Fragment>
|
||||
{hasExpressionErrors ? (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
<EuiCallOut color="danger" size="s" title={expressionErrorMessage} />
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
) : null}
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.selectEntity"
|
||||
defaultMessage="Select entity"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EntityIndexExpression
|
||||
dateField={dateField}
|
||||
geoField={geoField}
|
||||
errors={errors}
|
||||
setAlertParamsDate={(_date) => setRuleParams('dateField', _date)}
|
||||
setAlertParamsGeoField={(_geoField) => setRuleParams('geoField', _geoField)}
|
||||
setRuleProperty={setRuleProperty}
|
||||
setIndexPattern={setIndexPattern}
|
||||
indexPattern={indexPattern}
|
||||
isInvalid={!indexId || !dateField || !geoField}
|
||||
data={data}
|
||||
unifiedSearch={unifiedSearch}
|
||||
/>
|
||||
<EntityByExpression
|
||||
errors={errors}
|
||||
entity={entity}
|
||||
setAlertParamsEntity={(entityName) => setRuleParams('entity', entityName)}
|
||||
indexFields={indexPattern.fields}
|
||||
isInvalid={indexId && dateField && geoField ? !entity : false}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexItem>
|
||||
<QueryStringInput
|
||||
disableAutoFocus
|
||||
bubbleSubmitEvent
|
||||
indexPatterns={indexPattern ? [indexPattern] : []}
|
||||
query={indexQueryInput}
|
||||
onChange={(query) => {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.selectBoundaryIndex"
|
||||
defaultMessage="Select boundary"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<BoundaryIndexExpression
|
||||
ruleParams={ruleParams}
|
||||
errors={errors}
|
||||
boundaryIndexPattern={boundaryIndexPattern}
|
||||
setBoundaryIndexPattern={setBoundaryIndexPattern}
|
||||
setBoundaryGeoField={(_geoField: string | undefined) =>
|
||||
_geoField && setRuleParams('boundaryGeoField', _geoField)
|
||||
}
|
||||
setBoundaryNameField={(_boundaryNameField: string | undefined) =>
|
||||
_boundaryNameField
|
||||
? setRuleParams('boundaryNameField', _boundaryNameField)
|
||||
: setRuleParams('boundaryNameField', '')
|
||||
}
|
||||
boundaryNameField={boundaryNameField}
|
||||
data={data}
|
||||
unifiedSearch={unifiedSearch}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexItem>
|
||||
<QueryStringInput
|
||||
disableAutoFocus
|
||||
bubbleSubmitEvent
|
||||
indexPatterns={boundaryIndexPattern ? [boundaryIndexPattern] : []}
|
||||
query={boundaryIndexQueryInput}
|
||||
onChange={(query) => {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { GeoContainmentAlertTypeExpression as default };
|
|
@ -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`] = `
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error="Data view does not contain any allowed geospatial fields. Must have one of type geo_point."
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={true}
|
||||
label="Data view"
|
||||
labelType="label"
|
||||
>
|
||||
<MockIndexPatternSelectComponent
|
||||
fieldTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
]
|
||||
}
|
||||
indexPatternId="foobar_without_geopoint"
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isInvalid={true}
|
||||
onChange={[Function]}
|
||||
onNoIndexPatterns={[Function]}
|
||||
placeholder="Select data view"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`should render without error after mounting 1`] = `
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error=""
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Data view"
|
||||
labelType="label"
|
||||
>
|
||||
<MockIndexPatternSelectComponent
|
||||
fieldTypes={
|
||||
Array [
|
||||
"geo_point",
|
||||
]
|
||||
}
|
||||
indexPatternId="foobar_with_geopoint"
|
||||
isClearable={false}
|
||||
isDisabled={false}
|
||||
isInvalid={false}
|
||||
onChange={[Function]}
|
||||
onNoIndexPatterns={[Function]}
|
||||
placeholder="Select data view"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
`;
|
|
@ -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 (
|
||||
<EuiPopover
|
||||
id="popoverForExpression"
|
||||
button={
|
||||
<EuiExpression
|
||||
display="columns"
|
||||
data-test-subj="selectIndexExpression"
|
||||
description={expressionDescription}
|
||||
value={value || defaultValue}
|
||||
isActive={popoverOpen}
|
||||
onClick={() => setPopoverOpen(true)}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
}
|
||||
isOpen={popoverOpen}
|
||||
closePopover={() => setPopoverOpen(false)}
|
||||
ownFocus
|
||||
anchorPosition="downLeft"
|
||||
zIndex={8000}
|
||||
display="block"
|
||||
>
|
||||
<div style={{ width: '450px' }}>
|
||||
<EuiPopoverTitle>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>{expressionDescription}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="closePopover"
|
||||
iconType="cross"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.ui.expressionPopover.closePopoverLabel',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
)}
|
||||
onClick={() => setPopoverOpen(false)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
{popoverContent}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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(
|
||||
<GeoIndexPatternSelect
|
||||
http={{} as unknown as HttpSetup}
|
||||
onChange={() => {}}
|
||||
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(
|
||||
<GeoIndexPatternSelect
|
||||
http={{} as unknown as HttpSetup}
|
||||
onChange={() => {}}
|
||||
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();
|
||||
});
|
|
@ -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<Props, State> {
|
||||
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 (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.stackAlerts.geoContainment.noIndexPattern.messageTitle', {
|
||||
defaultMessage: `Couldn't find any data views`,
|
||||
})}
|
||||
color="warning"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.noIndexPattern.doThisPrefixDescription"
|
||||
defaultMessage="You'll need to "
|
||||
/>
|
||||
<EuiLink href={this.props.http.basePath.prepend(`/app/management/kibana/dataViews`)}>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.noIndexPattern.doThisLinkTextDescription"
|
||||
defaultMessage="Create a data view."
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.noIndexPattern.hintDescription"
|
||||
defaultMessage="Don't have any data? "
|
||||
/>
|
||||
<EuiLink
|
||||
href={this.props.http.basePath.prepend('/app/home#/tutorial_directory/sampleData')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.noIndexPattern.getStartedLinkText"
|
||||
defaultMessage="Get started with some sample data sets."
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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()}
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.indexPatternSelectLabel', {
|
||||
defaultMessage: 'Data view',
|
||||
})}
|
||||
isInvalid={isIndexPatternInvalid}
|
||||
error={error}
|
||||
>
|
||||
{IndexPatternSelectComponent ? (
|
||||
<IndexPatternSelectComponent
|
||||
isInvalid={isIndexPatternInvalid}
|
||||
isDisabled={this.state.noIndexPatternsExist}
|
||||
indexPatternId={this.props.value}
|
||||
onChange={this._onIndexPatternSelect}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.indexPatternSelectPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select data view',
|
||||
}
|
||||
)}
|
||||
fieldTypes={this.props.includedGeoTypes}
|
||||
onNoIndexPatterns={this._onNoIndexPatterns}
|
||||
isClearable={false}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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: () => <div>mock query input</div>,
|
||||
};
|
||||
});
|
||||
|
||||
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 '<div>mock IndexPatternSelect</div>';
|
||||
},
|
||||
},
|
||||
} as unknown as UnifiedSearchPublicPluginStart,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(<BoundaryForm {...props} />);
|
||||
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();
|
||||
});
|
|
@ -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<undefined | DataView>();
|
||||
const [dataViewNotFound, setDataViewNotFound] = useState(false);
|
||||
const [geoFields, setGeoFields] = useState<DataViewField[]>([]);
|
||||
const [nameFields, setNameFields] = useState<DataViewField[]>([]);
|
||||
|
||||
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 (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.boundariesFormLabel"
|
||||
defaultMessage="Boundaries"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiSkeletonText lines={3} size="s" isLoading={isLoading}>
|
||||
<EuiFormRow
|
||||
error={dataViewError}
|
||||
isInvalid={Boolean(dataViewError)}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.dataViewLabel', {
|
||||
defaultMessage: 'Data view',
|
||||
})}
|
||||
>
|
||||
<DataViewSelect
|
||||
dataViewId={props.ruleParams.boundaryIndexId}
|
||||
data={props.data}
|
||||
isInvalid={Boolean(dataViewError)}
|
||||
onChange={(nextDataView: DataView) => {
|
||||
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}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{props.ruleParams.boundaryIndexId && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
error={geoFieldError}
|
||||
isInvalid={Boolean(geoFieldError)}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.geofieldLabel', {
|
||||
defaultMessage: 'Location',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
isInvalid={Boolean(geoFieldError)}
|
||||
placeholder={i18n.translate('xpack.stackAlerts.geoContainment.selectGeoLabel', {
|
||||
defaultMessage: 'Select location field',
|
||||
})}
|
||||
value={props.ruleParams.boundaryGeoField}
|
||||
onChange={(fieldName?: string) => {
|
||||
if (fieldName) {
|
||||
props.setGeoField(fieldName);
|
||||
}
|
||||
}}
|
||||
fields={geoFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.boundaryNameSelectLabel', {
|
||||
defaultMessage: 'Display name (optional)',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
placeholder={i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.selectBoundaryNameLabel',
|
||||
{
|
||||
defaultMessage: 'Select name field',
|
||||
}
|
||||
)}
|
||||
value={props.ruleParams.boundaryNameField}
|
||||
onChange={(fieldName?: string) => {
|
||||
if (fieldName) {
|
||||
props.setNameField(fieldName);
|
||||
}
|
||||
}}
|
||||
fields={nameFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.boundariesFilterHelpText',
|
||||
{
|
||||
defaultMessage: 'Add a filter to narrow boundaries.',
|
||||
}
|
||||
)}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.filterLabel', {
|
||||
defaultMessage: 'Filter',
|
||||
})}
|
||||
>
|
||||
<QueryInput
|
||||
dataView={dataView}
|
||||
onChange={(query: Query) => {
|
||||
props.setQuery(query);
|
||||
}}
|
||||
query={props.ruleParams.boundaryIndexQuery}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
)}
|
||||
</EuiSkeletonText>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<props.unifiedSearch.ui.IndexPatternSelect
|
||||
isClearable={false}
|
||||
isDisabled={isLoading}
|
||||
isInvalid={props.isInvalid}
|
||||
isLoading={isLoading}
|
||||
indexPatternId={props.dataViewId ? props.dataViewId : ''}
|
||||
onChange={async (dataViewId?: string) => {
|
||||
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',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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: () => <div>mock query input</div>,
|
||||
};
|
||||
});
|
||||
|
||||
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 '<div>mock IndexPatternSelect</div>';
|
||||
},
|
||||
},
|
||||
} as unknown as UnifiedSearchPublicPluginStart,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(<EntityForm {...props} />);
|
||||
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();
|
||||
});
|
|
@ -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<undefined | DataView>();
|
||||
const [dataViewNotFound, setDataViewNotFound] = useState(false);
|
||||
const [dateFields, setDateFields] = useState<DataViewField[]>([]);
|
||||
const [entityFields, setEntityFields] = useState<DataViewField[]>([]);
|
||||
const [geoFields, setGeoFields] = useState<DataViewField[]>([]);
|
||||
|
||||
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 (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.geoContainment.entitiesFormLabel"
|
||||
defaultMessage="Entities"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiSkeletonText lines={3} size="s" isLoading={isLoading}>
|
||||
<EuiFormRow
|
||||
error={dataViewError}
|
||||
isInvalid={Boolean(dataViewError)}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.dataViewLabel', {
|
||||
defaultMessage: 'Data view',
|
||||
})}
|
||||
>
|
||||
<DataViewSelect
|
||||
dataViewId={props.ruleParams.indexId}
|
||||
data={props.data}
|
||||
isInvalid={Boolean(dataViewError)}
|
||||
onChange={(nextDataView: DataView) => {
|
||||
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}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{props.ruleParams.indexId && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
error={dateFieldError}
|
||||
isInvalid={Boolean(dateFieldError)}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.timeFieldLabel', {
|
||||
defaultMessage: 'Time',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
isInvalid={Boolean(dateFieldError)}
|
||||
placeholder={i18n.translate('xpack.stackAlerts.geoContainment.selectTimeLabel', {
|
||||
defaultMessage: 'Select time field',
|
||||
})}
|
||||
value={props.ruleParams.dateField}
|
||||
onChange={(fieldName?: string) => {
|
||||
if (fieldName) {
|
||||
props.setDateField(fieldName);
|
||||
}
|
||||
}}
|
||||
fields={dateFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
error={geoFieldError}
|
||||
isInvalid={Boolean(geoFieldError)}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.geofieldLabel', {
|
||||
defaultMessage: 'Location',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
isInvalid={Boolean(geoFieldError)}
|
||||
placeholder={i18n.translate('xpack.stackAlerts.geoContainment.selectGeoLabel', {
|
||||
defaultMessage: 'Select location field',
|
||||
})}
|
||||
value={props.ruleParams.geoField}
|
||||
onChange={(fieldName?: string) => {
|
||||
if (fieldName) {
|
||||
props.setGeoField(fieldName);
|
||||
}
|
||||
}}
|
||||
fields={geoFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
error={entityFieldError}
|
||||
isInvalid={Boolean(entityFieldError)}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.entityfieldLabel', {
|
||||
defaultMessage: 'Entity',
|
||||
})}
|
||||
>
|
||||
<SingleFieldSelect
|
||||
isInvalid={Boolean(entityFieldError)}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.stackAlerts.geoContainment.topHitsSplitFieldSelectPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select entity field',
|
||||
}
|
||||
)}
|
||||
value={props.ruleParams.entity}
|
||||
onChange={(fieldName?: string) => {
|
||||
if (fieldName) {
|
||||
props.setEntityField(fieldName);
|
||||
}
|
||||
}}
|
||||
fields={entityFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate('xpack.stackAlerts.geoContainment.entityFilterHelpText', {
|
||||
defaultMessage: 'Add a filter to narrow entities.',
|
||||
})}
|
||||
label={i18n.translate('xpack.stackAlerts.geoContainment.filterLabel', {
|
||||
defaultMessage: 'Filter',
|
||||
})}
|
||||
>
|
||||
<QueryInput
|
||||
dataView={dataView}
|
||||
onChange={(query: Query) => {
|
||||
props.setQuery(query);
|
||||
}}
|
||||
query={props.ruleParams.indexQuery}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
)}
|
||||
</EuiSkeletonText>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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<Query>(
|
||||
props.query || {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryStringInput
|
||||
disableAutoFocus
|
||||
bubbleSubmitEvent
|
||||
indexPatterns={props.dataView ? [props.dataView] : []}
|
||||
query={localQuery}
|
||||
onChange={(query) => {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<GeoContainmentAlertParams>
|
||||
> = (props) => {
|
||||
function getValidationError(key: string) {
|
||||
return props.errors[key]?.length > 0 && key in props.ruleParams
|
||||
? (props.errors[key] as string[])[0]
|
||||
: null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EntityForm
|
||||
data={props.data}
|
||||
getValidationError={getValidationError}
|
||||
ruleParams={props.ruleParams}
|
||||
setDataViewId={(id: string) => 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}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<BoundaryForm
|
||||
data={props.data}
|
||||
getValidationError={getValidationError}
|
||||
ruleParams={props.ruleParams}
|
||||
setDataViewId={(id: string) => {
|
||||
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}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -33,13 +33,14 @@ function fieldsToOptions(fields?: DataViewField[]): Array<EuiComboBoxOptionOptio
|
|||
}
|
||||
|
||||
interface Props {
|
||||
isInvalid?: boolean;
|
||||
placeholder: string;
|
||||
value: string | null; // data view field name
|
||||
value: string | null | undefined;
|
||||
onChange: (fieldName?: string) => void;
|
||||
fields: DataViewField[];
|
||||
}
|
||||
|
||||
export function SingleFieldSelect({ placeholder, value, onChange, fields }: Props) {
|
||||
export function SingleFieldSelect({ isInvalid, placeholder, value, onChange, fields }: Props) {
|
||||
function renderOption(
|
||||
option: EuiComboBoxOptionOption<DataViewField>,
|
||||
searchValue: string,
|
||||
|
@ -71,6 +72,7 @@ export function SingleFieldSelect({ placeholder, value, onChange, fields }: Prop
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
isInvalid={isInvalid}
|
||||
singleSelection={true}
|
||||
options={fieldsToOptions(fields)}
|
||||
selectedOptions={selectedOptions}
|
||||
|
@ -79,7 +81,6 @@ export function SingleFieldSelect({ placeholder, value, onChange, fields }: Prop
|
|||
renderOption={renderOption}
|
||||
isClearable={false}
|
||||
placeholder={placeholder}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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'];
|
||||
|
|
|
@ -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.'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": "アラートがしきい値を超えた日付。",
|
||||
|
|
|
@ -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": "告警超过阈值的日期。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue