[Alerting] Moves the Index & Geo Threshold UIs into the Stack Alerts Public Plugin (#82951)

This PR includes the following refactors:
1. Moves the Index Pattern Api from _Stack Alerts_ to the _Server_ plugin of _Trigger Actions UI_. This fixes a potential bug where a user could disable the _Stack Alerts_ plugin and inadvertently break the UI of the _ES Index _ action type.
2. Extracts the UI components for _Index Threshold_ and _Geo Threshold_ from the _Trigger Actions UI_ plugin and moves them into _Stack Alerts_.
This commit is contained in:
Gidi Meir Morris 2020-11-12 16:39:40 +00:00 committed by GitHub
parent 208e86e66a
commit ab72206da3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1212 additions and 896 deletions

View file

@ -1035,12 +1035,19 @@ module.exports = {
* Alerting Services overrides
*/
{
// typescript only for front and back end
// typescript for front and back end
files: ['x-pack/plugins/{alerts,stack_alerts,actions,task_manager,event_log}/**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
},
},
{
// typescript only for back end
files: ['x-pack/plugins/triggers_actions_ui/server/**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
},
},
/**
* Lens overrides

View file

@ -102,3 +102,4 @@ pageLoadAssetSize:
visualizations: 295025
visualize: 57431
watcher: 43598
stackAlerts: 29684

View file

@ -8,6 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
enableGeoTrackingThresholdAlert: schema.boolean({ defaultValue: false }),
});
export type Config = TypeOf<typeof configSchema>;

View file

@ -3,5 +3,5 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './config';
export const STACK_ALERTS_FEATURE_ID = 'stackAlerts';

View file

@ -3,7 +3,7 @@
"server": true,
"version": "8.0.0",
"kibanaVersion": "kibana",
"requiredPlugins": ["alerts", "features"],
"requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact"],
"configPath": ["xpack", "stack_alerts"],
"ui": false
"ui": true
}

View file

@ -5,18 +5,17 @@
*/
import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { AlertTypeModel } from '../../../../types';
import { validateExpression } from './validation';
import { GeoThresholdAlertParams } from './types';
import { AlertsContextValue } from '../../../context/alerts_context';
import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public';
export function getAlertType(): AlertTypeModel<GeoThresholdAlertParams, AlertsContextValue> {
return {
id: '.geo-threshold',
name: i18n.translate('xpack.triggersActionsUI.geoThreshold.name.trackingThreshold', {
name: i18n.translate('xpack.stackAlerts.geoThreshold.name.trackingThreshold', {
defaultMessage: 'Tracking threshold',
}),
description: i18n.translate('xpack.triggersActionsUI.geoThreshold.descriptionText', {
description: i18n.translate('xpack.stackAlerts.geoThreshold.descriptionText', {
defaultMessage: 'Alert when an entity enters or leaves a geo boundary.',
}),
iconClass: 'globe',

View file

@ -7,14 +7,13 @@
import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IErrorObject } from '../../../../../../types';
import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public';
import { ES_GEO_SHAPE_TYPES, GeoThresholdAlertParams } from '../../types';
import { AlertsContextValue } from '../../../../../context/alerts_context';
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';
import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields';
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns';
import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
interface Props {
alertParams: GeoThresholdAlertParams;
@ -117,12 +116,12 @@ export const BoundaryIndexExpression: FunctionComponent<Props> = ({
<EuiFormRow
id="geoField"
fullWidth
label={i18n.translate('xpack.triggersActionsUI.geoThreshold.geofieldLabel', {
label={i18n.translate('xpack.stackAlerts.geoThreshold.geofieldLabel', {
defaultMessage: 'Geospatial field',
})}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.triggersActionsUI.geoThreshold.selectLabel', {
placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.selectLabel', {
defaultMessage: 'Select geo field',
})}
value={boundaryGeoField}
@ -133,12 +132,12 @@ export const BoundaryIndexExpression: FunctionComponent<Props> = ({
<EuiFormRow
id="boundaryNameFieldSelect"
fullWidth
label={i18n.translate('xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel', {
label={i18n.translate('xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel', {
defaultMessage: 'Human-readable boundary name (optional)',
})}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.triggersActionsUI.geoThreshold.boundaryNameSelect', {
placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.boundaryNameSelect', {
defaultMessage: 'Select boundary name',
})}
value={boundaryNameField || null}
@ -156,7 +155,7 @@ export const BoundaryIndexExpression: FunctionComponent<Props> = ({
defaultValue={'Select an index pattern and geo shape field'}
value={boundaryIndexPattern.title}
popoverContent={indexPopover}
expressionDescription={i18n.translate('xpack.triggersActionsUI.geoThreshold.indexLabel', {
expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.indexLabel', {
defaultMessage: 'index',
})}
/>

View file

@ -8,10 +8,10 @@ import React, { FunctionComponent, useEffect, useRef } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { IErrorObject } from '../../../../../../types';
import { IErrorObject } from '../../../../../../triggers_actions_ui/public';
import { SingleFieldSelect } from '../util_components/single_field_select';
import { ExpressionWithPopover } from '../util_components/expression_with_popover';
import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields';
import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields';
interface Props {
errors: IErrorObject;
@ -59,7 +59,7 @@ export const EntityByExpression: FunctionComponent<Props> = ({
<EuiFormRow id="entitySelect" fullWidth error={errors.index}>
<SingleFieldSelect
placeholder={i18n.translate(
'xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder',
'xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder',
{
defaultMessage: 'Select entity field',
}
@ -77,7 +77,7 @@ export const EntityByExpression: FunctionComponent<Props> = ({
value={entity}
defaultValue={'Select entity field'}
popoverContent={indexPopover}
expressionDescription={i18n.translate('xpack.triggersActionsUI.geoThreshold.entityByLabel', {
expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityByLabel', {
defaultMessage: 'by',
})}
/>

View file

@ -8,14 +8,13 @@ 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 { IErrorObject } from '../../../../../../types';
import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public';
import { ES_GEO_FIELD_TYPES } from '../../types';
import { AlertsContextValue } from '../../../../../context/alerts_context';
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';
import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields';
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns';
import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
interface Props {
dateField: string;
@ -105,13 +104,13 @@ export const EntityIndexExpression: FunctionComponent<Props> = ({
fullWidth
label={
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.timeFieldLabel"
id="xpack.stackAlerts.geoThreshold.timeFieldLabel"
defaultMessage="Time field"
/>
}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.triggersActionsUI.geoThreshold.selectTimeLabel', {
placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.selectTimeLabel', {
defaultMessage: 'Select time field',
})}
value={timeField}
@ -124,12 +123,12 @@ export const EntityIndexExpression: FunctionComponent<Props> = ({
<EuiFormRow
id="geoField"
fullWidth
label={i18n.translate('xpack.triggersActionsUI.geoThreshold.geofieldLabel', {
label={i18n.translate('xpack.stackAlerts.geoThreshold.geofieldLabel', {
defaultMessage: 'Geospatial field',
})}
>
<SingleFieldSelect
placeholder={i18n.translate('xpack.triggersActionsUI.geoThreshold.selectGeoLabel', {
placeholder={i18n.translate('xpack.stackAlerts.geoThreshold.selectGeoLabel', {
defaultMessage: 'Select geo field',
})}
value={geoField}
@ -148,12 +147,9 @@ export const EntityIndexExpression: FunctionComponent<Props> = ({
value={indexPattern.title}
defaultValue={'Select an index pattern and geo shape/point field'}
popoverContent={indexPopover}
expressionDescription={i18n.translate(
'xpack.triggersActionsUI.geoThreshold.entityIndexLabel',
{
defaultMessage: 'index',
}
)}
expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityIndexLabel', {
defaultMessage: 'index',
})}
/>
);
};

View file

@ -19,15 +19,17 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { AlertTypeParamsExpressionProps } from '../../../../../types';
import {
AlertTypeParamsExpressionProps,
getTimeOptions,
AlertsContextValue,
} from '../../../../../triggers_actions_ui/public';
import { GeoThresholdAlertParams, TrackingEvent } from '../types';
import { AlertsContextValue } from '../../../../context/alerts_context';
import { ExpressionWithPopover } from './util_components/expression_with_popover';
import { EntityIndexExpression } from './expressions/entity_index_expression';
import { EntityByExpression } from './expressions/entity_by_expression';
import { BoundaryIndexExpression } from './expressions/boundary_index_expression';
import { IIndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns';
import { getTimeOptions } from '../../../../../common/lib/get_time_options';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns';
const DEFAULT_VALUES = {
TRACKING_EVENT: '',
@ -45,20 +47,20 @@ const DEFAULT_VALUES = {
};
const conditionOptions = Object.keys(TrackingEvent).map((key) => ({
text: (TrackingEvent as any)[key],
value: (TrackingEvent as any)[key],
text: TrackingEvent[key as TrackingEvent],
value: TrackingEvent[key as TrackingEvent],
}));
const labelForDelayOffset = (
<>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.delayOffset"
id="xpack.stackAlerts.geoThreshold.delayOffset"
defaultMessage="Delayed evaluation offset"
/>{' '}
<EuiIconTip
position="right"
type="questionInCircle"
content={i18n.translate('xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip', {
content={i18n.translate('xpack.stackAlerts.geoThreshold.delayOffsetTooltip', {
defaultMessage: 'Evaluate alerts on a delayed cycle to adjust for data latency',
})}
/>
@ -125,7 +127,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeP
const hasExpressionErrors = false;
const expressionErrorMessage = i18n.translate(
'xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage',
'xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage',
{
defaultMessage: 'Expression contains errors.',
}
@ -180,7 +182,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeP
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.selectOffset"
id="xpack.stackAlerts.geoThreshold.selectOffset"
defaultMessage="Select offset (optional)"
/>
</h5>
@ -221,7 +223,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeP
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.selectEntity"
id="xpack.stackAlerts.geoThreshold.selectEntity"
defaultMessage="Select entity"
/>
</h5>
@ -251,7 +253,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeP
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.selectIndex"
id="xpack.stackAlerts.geoThreshold.selectIndex"
defaultMessage="Define the condition"
/>
</h5>
@ -280,19 +282,16 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<AlertTypeP
</div>
</EuiFormRow>
}
expressionDescription={i18n.translate(
'xpack.triggersActionsUI.geoThreshold.whenEntityLabel',
{
defaultMessage: 'when entity',
}
)}
expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.whenEntityLabel', {
defaultMessage: 'when entity',
})}
/>
<EuiSpacer size="l" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex"
id="xpack.stackAlerts.geoThreshold.selectBoundaryIndex"
defaultMessage="Select boundary:"
/>
</h5>

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import React, { ReactNode, useState } from 'react';
import {
EuiButtonIcon,
EuiExpression,
@ -22,10 +22,10 @@ export const ExpressionWithPopover: ({
value,
isInvalid,
}: {
popoverContent: any;
expressionDescription: any;
defaultValue?: any;
value?: any;
popoverContent: ReactNode;
expressionDescription: ReactNode;
defaultValue?: ReactNode;
value?: ReactNode;
isInvalid?: boolean;
}) => JSX.Element = ({ popoverContent, expressionDescription, defaultValue, value, isInvalid }) => {
const [popoverOpen, setPopoverOpen] = useState(false);
@ -61,7 +61,7 @@ export const ExpressionWithPopover: ({
iconType="cross"
color="danger"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel',
'xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel',
{
defaultMessage: 'Close',
}

View file

@ -14,6 +14,7 @@ import { HttpSetup } from 'kibana/public';
interface Props {
onChange: (indexPattern: IndexPattern) => void;
value: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
IndexPatternSelectComponent: any;
indexPatternService: IndexPatternsContract | undefined;
http: HttpSetup;
@ -39,7 +40,7 @@ export class GeoIndexPatternSelect extends Component<Props, State> {
this._isMounted = true;
}
_onIndexPatternSelect = async (indexPatternId: any) => {
_onIndexPatternSelect = async (indexPatternId: string) => {
if (!indexPatternId || indexPatternId.length === 0 || !this.props.indexPatternService) {
return;
}
@ -70,42 +71,39 @@ export class GeoIndexPatternSelect extends Component<Props, State> {
return (
<>
<EuiCallOut
title={i18n.translate(
'xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle',
{
defaultMessage: `Couldn't find any index patterns with geospatial fields`,
}
)}
title={i18n.translate('xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle', {
defaultMessage: `Couldn't find any index patterns with geospatial fields`,
})}
color="warning"
>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription"
id="xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription"
defaultMessage="You'll need to "
/>
<EuiLink
href={this.props.http.basePath.prepend(`/app/management/kibana/indexPatterns`)}
>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription"
id="xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription"
defaultMessage="create an index pattern"
/>
</EuiLink>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription"
id="xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription"
defaultMessage=" with geospatial fields."
/>
</p>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription"
id="xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription"
defaultMessage="Don't have any geospatial data sets? "
/>
<EuiLink
href={this.props.http.basePath.prepend('/app/home#/tutorial_directory/sampleData')}
>
<FormattedMessage
id="xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText"
id="xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText"
defaultMessage="Get started with some sample data sets."
/>
</EuiLink>
@ -123,7 +121,7 @@ export class GeoIndexPatternSelect extends Component<Props, State> {
{this._renderNoIndexPatternWarning()}
<EuiFormRow
label={i18n.translate('xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel', {
label={i18n.translate('xpack.stackAlerts.geoThreshold.indexPatternSelectLabel', {
defaultMessage: 'Index pattern',
})}
>
@ -133,7 +131,7 @@ export class GeoIndexPatternSelect extends Component<Props, State> {
indexPatternId={this.props.value}
onChange={this._onIndexPatternSelect}
placeholder={i18n.translate(
'xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder',
'xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder',
{
defaultMessage: 'Select index pattern',
}

View file

@ -14,7 +14,7 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { IFieldType } from 'src/plugins/data/public';
import { FieldIcon } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public';
function fieldsToOptions(fields?: IFieldType[]): Array<EuiComboBoxOptionOption<IFieldType>> {
if (!fields) {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { ValidationResult } from '../../../../types';
import { ValidationResult } from '../../../../triggers_actions_ui/public';
import { GeoThresholdAlertParams } from './types';
export const validateExpression = (alertParams: GeoThresholdAlertParams): ValidationResult => {
@ -35,7 +35,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!index) {
errors.index.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText', {
defaultMessage: 'Index pattern is required.',
})
);
@ -43,7 +43,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!geoField) {
errors.geoField.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText', {
defaultMessage: 'Geo field is required.',
})
);
@ -51,7 +51,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!entity) {
errors.entity.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredEntityText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredEntityText', {
defaultMessage: 'Entity is required.',
})
);
@ -59,7 +59,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!dateField) {
errors.dateField.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredDateFieldText', {
defaultMessage: 'Date field is required.',
})
);
@ -67,7 +67,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!trackingEvent) {
errors.trackingEvent.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText', {
defaultMessage: 'Tracking event is required.',
})
);
@ -75,7 +75,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!boundaryType) {
errors.boundaryType.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText', {
defaultMessage: 'Boundary type is required.',
})
);
@ -83,7 +83,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!boundaryIndexTitle) {
errors.boundaryIndexTitle.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText', {
defaultMessage: 'Boundary index pattern title is required.',
})
);
@ -91,7 +91,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida
if (!boundaryGeoField) {
errors.boundaryGeoField.push(
i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText', {
i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText', {
defaultMessage: 'Boundary geo field is required.',
})
);

View file

@ -6,18 +6,17 @@
import { getAlertType as getGeoThresholdAlertType } from './geo_threshold';
import { getAlertType as getThresholdAlertType } from './threshold';
import { TypeRegistry } from '../../type_registry';
import { AlertTypeModel } from '../../../types';
import { TriggersActionsUiConfigType } from '../../../plugin';
import { Config } from '../../common';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
export function registerBuiltInAlertTypes({
export function registerAlertTypes({
alertTypeRegistry,
triggerActionsUiConfig,
config,
}: {
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
triggerActionsUiConfig: TriggersActionsUiConfigType;
alertTypeRegistry: TriggersAndActionsUIPublicPluginSetup['alertTypeRegistry'];
config: Config;
}) {
if (triggerActionsUiConfig.enableGeoTrackingThresholdAlert) {
if (config.enableGeoTrackingThresholdAlert) {
alertTypeRegistry.register(getGeoThresholdAlertType());
}
alertTypeRegistry.register(getThresholdAlertType());

View file

@ -29,21 +29,20 @@ import {
getIndexPatterns,
getIndexOptions,
getFields,
} from '../../../../common/index_controls';
import { COMPARATORS, builtInComparators } from '../../../../common/constants';
import { getTimeFieldOptions } from '../../../../common/lib/get_time_options';
import { ThresholdVisualization } from './visualization';
import { WhenExpression } from '../../../../common';
import {
COMPARATORS,
builtInComparators,
getTimeFieldOptions,
OfExpression,
ThresholdExpression,
ForLastExpression,
GroupByExpression,
} from '../../../../common';
import { builtInAggregationTypes } from '../../../../common/constants';
WhenExpression,
builtInAggregationTypes,
AlertTypeParamsExpressionProps,
AlertsContextValue,
} from '../../../../triggers_actions_ui/public';
import { ThresholdVisualization } from './visualization';
import { IndexThresholdAlertParams } from './types';
import { AlertTypeParamsExpressionProps } from '../../../../types';
import { AlertsContextValue } from '../../../context/alerts_context';
import './expression.scss';
const DEFAULT_VALUES = {
@ -89,7 +88,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
const [indexPopoverOpen, setIndexPopoverOpen] = useState(false);
const [indexPatterns, setIndexPatterns] = useState([]);
const [esFields, setEsFields] = useState<Record<string, any>>([]);
const [esFields, setEsFields] = useState<unknown[]>([]);
const [indexOptions, setIndexOptions] = useState<EuiComboBoxOptionOption[]>([]);
const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]);
const [isIndiciesLoading, setIsIndiciesLoading] = useState<boolean>(false);
@ -98,7 +97,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
(errorKey) =>
expressionFieldsWithValidation.includes(errorKey) &&
errors[errorKey].length >= 1 &&
(alertParams as { [key: string]: any })[errorKey] !== undefined
alertParams[errorKey as keyof IndexThresholdAlertParams] !== undefined
);
const canShowVizualization = !!Object.keys(errors).find(
@ -106,7 +105,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
);
const expressionErrorMessage = i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage',
'xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage',
{
defaultMessage: 'Expression contains errors.',
}
@ -126,7 +125,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
if (index && index.length > 0) {
const currentEsFields = await getFields(http, index);
const timeFields = getTimeFieldOptions(currentEsFields as any);
const timeFields = getTimeFieldOptions(currentEsFields);
setEsFields(currentEsFields);
setTimeFieldOptions([firstFieldOption, ...timeFields]);
@ -159,7 +158,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
fullWidth
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.threshold.indicesToQueryLabel"
id="xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel"
defaultMessage="Indices to query"
/>
}
@ -167,7 +166,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
error={errors.index}
helpText={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription"
id="xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription"
defaultMessage="Use * to broaden your query."
/>
}
@ -211,7 +210,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
return;
}
const currentEsFields = await getFields(http, indices);
const timeFields = getTimeFieldOptions(currentEsFields as any);
const timeFields = getTimeFieldOptions(currentEsFields);
setEsFields(currentEsFields);
setTimeFieldOptions([firstFieldOption, ...timeFields]);
@ -233,7 +232,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
fullWidth
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldLabel"
id="xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel"
defaultMessage="Time field"
/>
}
@ -284,7 +283,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.selectIndex"
id="xpack.stackAlerts.threshold.ui.selectIndex"
defaultMessage="Select an index"
/>
</h5>
@ -296,12 +295,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
<EuiExpression
display="columns"
data-test-subj="selectIndexExpression"
description={i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.threshold.indexLabel',
{
defaultMessage: 'index',
}
)}
description={i18n.translate('xpack.stackAlerts.threshold.ui.alertParams.indexLabel', {
defaultMessage: 'index',
})}
value={index && index.length > 0 ? renderIndices(index) : firstFieldOption.text}
isActive={indexPopoverOpen}
onClick={() => {
@ -321,12 +317,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
<EuiPopoverTitle>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>
{i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel',
{
defaultMessage: 'index',
}
)}
{i18n.translate('xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel', {
defaultMessage: 'index',
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
@ -334,7 +327,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
iconType="cross"
color="danger"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel',
'xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel',
{
defaultMessage: 'Close',
}
@ -386,7 +379,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.conditionPrompt"
id="xpack.stackAlerts.threshold.ui.conditionPrompt"
defaultMessage="Define the condition"
/>
</h5>
@ -411,10 +404,10 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
timeWindowUnit={timeWindowUnit}
display="fullWidth"
errors={errors}
onChangeWindowSize={(selectedWindowSize: any) =>
onChangeWindowSize={(selectedWindowSize: number | undefined) =>
setAlertParams('timeWindowSize', selectedWindowSize)
}
onChangeWindowUnit={(selectedWindowUnit: any) =>
onChangeWindowUnit={(selectedWindowUnit: string) =>
setAlertParams('timeWindowUnit', selectedWindowUnit)
}
/>
@ -427,7 +420,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<AlertTyp
body={
<EuiText color="subdued">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription"
id="xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription"
defaultMessage="Complete the expression to generate a preview."
/>
</EuiText>

View file

@ -5,18 +5,17 @@
*/
import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { AlertTypeModel } from '../../../../types';
import { validateExpression } from './validation';
import { IndexThresholdAlertParams } from './types';
import { AlertsContextValue } from '../../../context/alerts_context';
import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public';
export function getAlertType(): AlertTypeModel<IndexThresholdAlertParams, AlertsContextValue> {
return {
id: '.index-threshold',
name: i18n.translate('xpack.triggersActionsUI.indexThresholdAlert.nameText', {
name: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.nameText', {
defaultMessage: 'Index threshold',
}),
description: i18n.translate('xpack.triggersActionsUI.indexThresholdAlert.descriptionText', {
description: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.descriptionText', {
defaultMessage: 'Alert when an aggregated query meets the threshold.',
}),
iconClass: 'alert',
@ -26,7 +25,7 @@ export function getAlertType(): AlertTypeModel<IndexThresholdAlertParams, Alerts
alertParamsExpression: lazy(() => import('./expression')),
validate: validateExpression,
defaultActionMessage: i18n.translate(
'xpack.triggersActionsUI.components.builtinAlertTypes.threshold.alertDefaultActionMessage',
'xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage',
{
defaultMessage: `alert \\{\\{alertName\\}\\} group \\{\\{context.group\\}\\} value \\{\\{context.value\\}\\} exceeded threshold \\{\\{context.function\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} on \\{\\{context.date\\}\\}`,
}

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup } from 'kibana/public';
import { TimeSeriesResult } from '../../../../triggers_actions_ui/common';
import { IndexThresholdAlertParams } from './types';
const INDEX_THRESHOLD_DATA_API_ROOT = '/api/triggers_actions_ui/data';
export interface GetThresholdAlertVisualizationDataParams {
model: IndexThresholdAlertParams;
visualizeOptions: {
rangeFrom: string;
rangeTo: string;
interval: string;
};
http: HttpSetup;
}
export async function getThresholdAlertVisualizationData({
model,
visualizeOptions,
http,
}: GetThresholdAlertVisualizationDataParams): Promise<TimeSeriesResult> {
const timeSeriesQueryParams = {
index: model.index,
timeField: model.timeField,
aggType: model.aggType,
aggField: model.aggField,
groupBy: model.groupBy,
termField: model.termField,
termSize: model.termSize,
timeWindowSize: model.timeWindowSize,
timeWindowUnit: model.timeWindowUnit,
dateStart: new Date(visualizeOptions.rangeFrom).toISOString(),
dateEnd: new Date(visualizeOptions.rangeTo).toISOString(),
interval: visualizeOptions.interval,
};
return await http.post<TimeSeriesResult>(`${INDEX_THRESHOLD_DATA_API_ROOT}/_time_series_query`, {
body: JSON.stringify(timeSeriesQueryParams),
});
}

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { ValidationResult } from '../../../../types';
import { IndexThresholdAlertParams } from './types';
import {
ValidationResult,
builtInGroupByTypes,
builtInAggregationTypes,
builtInComparators,
} from '../../../../common/constants';
} from '../../../../triggers_actions_ui/public';
export const validateExpression = (alertParams: IndexThresholdAlertParams): ValidationResult => {
const {
@ -39,21 +39,21 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali
validationResult.errors = errors;
if (!index || index.length === 0) {
errors.index.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText', {
defaultMessage: 'Index is required.',
})
);
}
if (!timeField) {
errors.timeField.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText', {
defaultMessage: 'Time field is required.',
})
);
}
if (aggType && builtInAggregationTypes[aggType].fieldRequired && !aggField) {
errors.aggField.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText', {
defaultMessage: 'Aggregation field is required.',
})
);
@ -65,7 +65,7 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali
!termSize
) {
errors.termSize.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText', {
defaultMessage: 'Term size is required.',
})
);
@ -77,21 +77,21 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali
!termField
) {
errors.termField.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText', {
defaultMessage: 'Term field is required.',
})
);
}
if (!timeWindowSize) {
errors.timeWindowSize.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText', {
defaultMessage: 'Time window size is required.',
})
);
}
if (!threshold || threshold.length === 0 || threshold[0] === undefined) {
errors.threshold0.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text', {
defaultMessage: 'Threshold0 is required.',
})
);
@ -104,14 +104,14 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali
(threshold && threshold.length < builtInComparators[thresholdComparator!].requiredValues))
) {
errors.threshold1.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text', {
defaultMessage: 'Threshold1 is required.',
})
);
}
if (threshold && threshold.length === 2 && threshold[0] > threshold[1]) {
errors.threshold1.push(
i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text', {
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text', {
defaultMessage: 'Threshold1 should be > Threshold0.',
})
);

View file

@ -29,11 +29,17 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { getThresholdAlertVisualizationData } from '../../../../common/lib/index_threshold_api';
import { AggregationType, Comparator } from '../../../../common/types';
import { AlertsContextValue } from '../../../context/alerts_context';
import {
getThresholdAlertVisualizationData,
GetThresholdAlertVisualizationDataParams,
} from './index_threshold_api';
import {
AlertsContextValue,
AggregationType,
Comparator,
} from '../../../../triggers_actions_ui/public';
import { IndexThresholdAlertParams } from './types';
import { parseDuration } from '../../../../../../alerts/common/parse_duration';
import { parseDuration } from '../../../../alerts/common/parse_duration';
const customTheme = () => {
return {
@ -125,7 +131,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext;
const [loadingState, setLoadingState] = useState<LoadingStateType | null>(null);
const [error, setError] = useState<undefined | any>(undefined);
const [error, setError] = useState<undefined | Error>(undefined);
const [visualizationData, setVisualizationData] = useState<Record<string, MetricResult[]>>();
const [startVisualizationAt, setStartVisualizationAt] = useState<Date>(new Date());
@ -150,7 +156,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
if (toastNotifications) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage',
'xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage',
{ defaultMessage: 'Unable to load visualization' }
),
});
@ -199,7 +205,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
body={
<EuiText color="subdued">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription"
id="xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription"
defaultMessage="Loading alert visualization…"
/>
</EuiText>
@ -215,7 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
<EuiCallOut
title={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle"
id="xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle"
defaultMessage="Cannot load alert visualization"
values={{}}
/>
@ -239,7 +245,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
const alertVisualizationDataKeys = Object.keys(visualizationData);
const timezone = getTimezone(uiSettings);
const actualThreshold = getThreshold();
let maxY = actualThreshold[actualThreshold.length - 1] as any;
let maxY = actualThreshold[actualThreshold.length - 1];
(Object.values(visualizationData) as number[][][]).forEach((data) => {
data.forEach(([, y]) => {
@ -288,14 +294,14 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
/>
);
})}
{actualThreshold.map((_value: any, i: number) => {
const specId = i === 0 ? 'threshold' : `threshold${i}`;
{actualThreshold.map((_value: number, thresholdIndex: number) => {
const specId = thresholdIndex === 0 ? 'threshold' : `threshold${thresholdIndex}`;
return (
<LineAnnotation
key={specId}
id={specId}
domainType={AnnotationDomainTypes.YDomain}
dataValues={[{ dataValue: threshold[i], details: specId }]}
dataValues={[{ dataValue: threshold[thresholdIndex], details: specId }]}
/>
);
})}
@ -305,14 +311,14 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
size="s"
title={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle"
id="xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle"
defaultMessage="No data matches this query"
/>
}
color="warning"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.dataDoesNotExistTextMessage"
id="xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage"
defaultMessage="Check that your time range and filters are correct."
/>
</EuiCallOut>
@ -325,7 +331,11 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
};
// convert the data from the visualization API into something easier to digest with charts
async function getVisualizationData(model: any, visualizeOptions: any, http: HttpSetup) {
async function getVisualizationData(
model: IndexThresholdAlertParams,
visualizeOptions: GetThresholdAlertVisualizationDataParams['visualizeOptions'],
http: HttpSetup
) {
const vizData = await getThresholdAlertVisualizationData({
model,
visualizeOptions,

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/public';
import { StackAlertsPublicPlugin } from './plugin';
export const plugin = (ctx: PluginInitializerContext) => new StackAlertsPublicPlugin(ctx);

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public';
import { registerAlertTypes } from './alert_types';
import { Config } from '../common';
export type Setup = void;
export type Start = void;
export interface StackAlertsPublicSetupDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
}
export class StackAlertsPublicPlugin implements Plugin<Setup, Start, StackAlertsPublicSetupDeps> {
private initializerContext: PluginInitializerContext;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
public setup(core: CoreSetup, { triggersActionsUi }: StackAlertsPublicSetupDeps) {
registerAlertTypes({
alertTypeRegistry: triggersActionsUi.alertTypeRegistry,
config: this.initializerContext.config.get<Config>(),
});
}
public start() {}
public stop() {}
}

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { Service } from '../../types';
import { Logger } from 'src/core/server';
import { STACK_ALERTS_FEATURE_ID } from '../../../common';
import { getGeoThresholdExecutor } from './geo_threshold';
import {
@ -173,7 +173,7 @@ export interface GeoThresholdParams {
}
export function getAlertType(
service: Omit<Service, 'indexThreshold'>
logger: Logger
): {
defaultActionGroupId: string;
actionGroups: ActionGroup[];
@ -222,7 +222,7 @@ export function getAlertType(
name: alertTypeName,
actionGroups: [{ id: ActionGroupId, name: actionGroupName }],
defaultActionGroupId: ActionGroupId,
executor: getGeoThresholdExecutor(service),
executor: getGeoThresholdExecutor(logger),
producer: STACK_ALERTS_FEATURE_ID,
validate: {
params: ParamsSchema,

View file

@ -6,7 +6,7 @@
import { ILegacyScopedClusterClient } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { Logger } from '../../types';
import { Logger } from 'src/core/server';
export const OTHER_CATEGORY = 'other';
// Consider dynamically obtaining from config?

View file

@ -6,10 +6,10 @@
import _ from 'lodash';
import { SearchResponse } from 'elasticsearch';
import { Logger } from 'src/core/server';
import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_query_builder';
import { AlertServices, AlertTypeState } from '../../../../alerts/server';
import { ActionGroupId, GEO_THRESHOLD_ID, GeoThresholdParams } from './alert_type';
import { Logger } from '../../types';
interface LatestEntityLocation {
location: number[];
@ -169,7 +169,7 @@ function getOffsetTime(delayOffsetWithUnits: string, oldTime: Date): Date {
return adjustedDate;
}
export const getGeoThresholdExecutor = ({ logger: log }: { logger: Logger }) =>
export const getGeoThresholdExecutor = (log: Logger) =>
async function ({
previousStartedAt,
startedAt,

View file

@ -4,15 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Service, AlertingSetup } from '../../types';
import { Logger } from 'src/core/server';
import { AlertingSetup } from '../../types';
import { getAlertType } from './alert_type';
interface RegisterParams {
service: Omit<Service, 'indexThreshold'>;
logger: Logger;
alerts: AlertingSetup;
}
export function register(params: RegisterParams) {
const { service, alerts } = params;
alerts.registerType(getAlertType(service));
const { logger, alerts } = params;
alerts.registerType(getAlertType(logger));
}

View file

@ -8,11 +8,9 @@ import { loggingSystemMock } from '../../../../../../../src/core/server/mocks';
import { getAlertType, GeoThresholdParams } from '../alert_type';
describe('alertType', () => {
const service = {
logger: loggingSystemMock.create().get(),
};
const logger = loggingSystemMock.create().get();
const alertType = getAlertType(service);
const alertType = getAlertType(logger);
it('alert type creation structure is the expected value', async () => {
expect(alertType.id).toBe('.geo-threshold');

View file

@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Service, IRouter, AlertingSetup } from '../types';
import { Logger } from 'src/core/server';
import { AlertingSetup, StackAlertsStartDeps } from '../types';
import { register as registerIndexThreshold } from './index_threshold';
import { register as registerGeoThreshold } from './geo_threshold';
interface RegisterAlertTypesParams {
service: Service;
router: IRouter;
logger: Logger;
data: Promise<StackAlertsStartDeps['triggersActionsUi']['data']>;
alerts: AlertingSetup;
baseRoute: string;
}
export function registerBuiltInAlertTypes(params: RegisterAlertTypesParams) {

View file

@ -13,10 +13,7 @@ is exceeded.
## alertType `.index-threshold`
The alertType parameters are specified in
[`lib/core_query_types.ts`][it-core-query]
and
[`alert_type_params.ts`][it-alert-params].
The alertType parameters are specified in [`alert_type_params.ts`][it-alert-params].
The alertType has a single actionGroup, `'threshold met'`. The `context` object
provided to actions is specified in
@ -123,227 +120,6 @@ server log [17:32:10.060] [warning][actions][actions][plugins] \
[now-iso]: https://github.com/pmuellr/now-iso
## http endpoints
## Data Apis via the TriggersActionsUi plugin and its http endpoints
The following endpoints are provided for this alert type:
- `POST /api/stack_alerts/index_threshold/_indices`
- `POST /api/stack_alerts/index_threshold/_fields`
- `POST /api/stack_alerts/index_threshold/_time_series_query`
### `POST .../_indices`
This HTTP endpoint is provided for the alerting ui to list the available
"index names" for the user to select to use with the alert. This API also
returns aliases which match the supplied pattern.
The request body is expected to be a JSON object in the following form, where the
`pattern` value may include comma-separated names and wildcards.
```js
{
pattern: "index-name-pattern"
}
```
The response body is a JSON object in the following form, where each element
of the `indices` array is the name of an index or alias. The number of elements
returned is limited, as this API is intended to be used to help narrow down
index names to use with the alert, and not support pagination, etc.
```js
{
indices: ["index-name-1", "alias-name-1", ...]
}
```
### `POST .../_fields`
This HTTP endpoint is provided for the alerting ui to list the available
fields for the user to select to use with the alert.
The request body is expected to be a JSON object in the following form, where the
`indexPatterns` array elements may include comma-separated names and wildcards.
```js
{
indexPatterns: ["index-pattern-1", "index-pattern-2"]
}
```
The response body is a JSON object in the following form, where each element
fields array is a field object.
```js
{
fields: [fieldObject1, fieldObject2, ...]
}
```
A field object is the following shape:
```typescript
{
name: string, // field name
type: string, // field type - eg 'keyword', 'date', 'long', etc
normalizedType: string, // for numeric types, this will be 'number'
aggregatable: true, // value from elasticsearch field capabilities
searchable: true, // value from elasticsearch field capabilities
}
```
### `POST .../_time_series_query`
This HTTP endpoint is provided to return the values the alertType would calculate,
over a series of time. It is intended to be used in the alerting UI to
provide a "preview" of the alert during creation/editing based on recent data,
and could be used to show a "simulation" of the the alert over an arbitrary
range of time.
The endpoint is `POST /api/stack_alerts/index_threshold/_time_series_query`.
The request and response bodies are specifed in
[`lib/core_query_types.ts`][it-core-query]
and
[`lib/time_series_types.ts`][it-timeSeries-types].
The request body is very similar to the alertType's parameters.
### example
Continuing with the example above, here's a query to get the values calculated
for the last 10 seconds.
This example uses [now-iso][] to generate iso date strings.
```console
curl -k "https://elastic:changeme@localhost:5601/api/stack_alerts/index_threshold/_time_series_query" \
-H "kbn-xsrf: foo" -H "content-type: application/json" -d "{
\"index\": \"es-hb-sim\",
\"timeField\": \"@timestamp\",
\"aggType\": \"avg\",
\"aggField\": \"summary.up\",
\"groupBy\": \"top\",
\"termSize\": 100,
\"termField\": \"monitor.name.keyword\",
\"interval\": \"1s\",
\"dateStart\": \"`now-iso -10s`\",
\"dateEnd\": \"`now-iso`\",
\"timeWindowSize\": 5,
\"timeWindowUnit\": \"s\"
}"
```
```
{
"results": [
{
"group": "host-A",
"metrics": [
[ "2020-02-26T15:10:40.000Z", 0 ],
[ "2020-02-26T15:10:41.000Z", 0 ],
[ "2020-02-26T15:10:42.000Z", 0 ],
[ "2020-02-26T15:10:43.000Z", 0 ],
[ "2020-02-26T15:10:44.000Z", 0 ],
[ "2020-02-26T15:10:45.000Z", 0 ],
[ "2020-02-26T15:10:46.000Z", 0 ],
[ "2020-02-26T15:10:47.000Z", 0 ],
[ "2020-02-26T15:10:48.000Z", 0 ],
[ "2020-02-26T15:10:49.000Z", 0 ],
[ "2020-02-26T15:10:50.000Z", 0 ]
]
}
]
}
```
To get the current value of the calculated metric, you can leave off the date:
```
curl -k "https://elastic:changeme@localhost:5601/api/stack_alerts/index_threshold/_time_series_query" \
-H "kbn-xsrf: foo" -H "content-type: application/json" -d '{
"index": "es-hb-sim",
"timeField": "@timestamp",
"aggType": "avg",
"aggField": "summary.up",
"groupBy": "top",
"termField": "monitor.name.keyword",
"termSize": 100,
"interval": "1s",
"timeWindowSize": 5,
"timeWindowUnit": "s"
}'
```
```
{
"results": [
{
"group": "host-A",
"metrics": [
[ "2020-02-26T15:23:36.635Z", 0 ]
]
}
]
}
```
[it-timeSeries-types]: lib/time_series_types.ts
## service functions
A single service function is available that provides the functionality
of the http endpoint `POST /api/stack_alerts/index_threshold/_time_series_query`,
but as an API for Kibana plugins. The function is available as
`alertingService.indexThreshold.timeSeriesQuery()`
The parameters and return value for the function are the same as for the HTTP
request, though some additional parameters are required (logger, callCluster,
etc).
## notes on the timeSeriesQuery API / http endpoint
This API provides additional parameters beyond what the alertType itself uses:
- `dateStart`
- `dateEnd`
- `interval`
The `dateStart` and `dateEnd` parameters are ISO date strings.
The `interval` parameter is intended to model the `interval` the alert is
currently using, and uses the same `1s`, `2m`, `3h`, etc format. Over the
supplied date range, a time-series data point will be calculated every
`interval` duration.
So the number of time-series points in the output of the API should be:
```
( dateStart - dateEnd ) / interval
```
Example:
```
dateStart: '2020-01-01T00:00:00'
dateEnd: '2020-01-02T00:00:00'
interval: '1h'
```
The date range is 1 day === 24 hours. The interval is 1 hour. So there should
be ~24 time series points in the output.
For preview purposes:
- The `termSize` parameter should be used to help cut
down on the amount of work ES does, and keep the generated graphs a little
simpler. Probably something like `10`.
- For queries with long date ranges, you probably don't want to use the
`interval` the alert is set to, as the `interval` used in the query, as this
could result in a lot of time-series points being generated, which is both
costly in ES, and may result in noisy graphs.
- The `timeWindow*` parameters should be the same as what the alert is using,
especially for the `count` and `sum` aggregation types. Those aggregations
don't scale the same way the others do, when the window changes. Even for
the other aggregations, changing the window could result in dramatically
different values being generated - `avg` will be more "average-y", `min`
and `max` will be a little stickier.
The Index Threshold Alert Type is backed by Apis exposed by the [TriggersActionsUi plugin](../../../../triggers_actions_ui/README.md).

View file

@ -9,14 +9,12 @@ import { getAlertType } from './alert_type';
import { Params } from './alert_type_params';
describe('alertType', () => {
const service = {
indexThreshold: {
timeSeriesQuery: jest.fn(),
},
logger: loggingSystemMock.create().get(),
const logger = loggingSystemMock.create().get();
const data = {
timeSeriesQuery: jest.fn(),
};
const alertType = getAlertType(service);
const alertType = getAlertType(logger, Promise.resolve(data));
it('alert type creation structure is the expected value', async () => {
expect(alertType.id).toBe('.index-threshold');

View file

@ -5,23 +5,26 @@
*/
import { i18n } from '@kbn/i18n';
import { AlertType, AlertExecutorOptions } from '../../types';
import { Logger } from 'src/core/server';
import { AlertType, AlertExecutorOptions, StackAlertsStartDeps } from '../../types';
import { Params, ParamsSchema } from './alert_type_params';
import { ActionContext, BaseActionContext, addMessages } from './action_context';
import { TimeSeriesQuery } from './lib/time_series_query';
import { Service } from '../../types';
import { STACK_ALERTS_FEATURE_ID } from '../../../common';
import {
CoreQueryParamsSchemaProperties,
TimeSeriesQuery,
} from '../../../../triggers_actions_ui/server';
export const ID = '.index-threshold';
import { CoreQueryParamsSchemaProperties } from './lib/core_query_types';
const ActionGroupId = 'threshold met';
const ComparatorFns = getComparatorFns();
export const ComparatorFnNames = new Set(ComparatorFns.keys());
export function getAlertType(service: Service): AlertType<Params, {}, {}, ActionContext> {
const { logger } = service;
export function getAlertType(
logger: Logger,
data: Promise<StackAlertsStartDeps['triggersActionsUi']['data']>
): AlertType<Params, {}, {}, ActionContext> {
const alertTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', {
defaultMessage: 'Index threshold',
});
@ -152,7 +155,7 @@ export function getAlertType(service: Service): AlertType<Params, {}, {}, Action
interval: undefined,
};
// console.log(`index_threshold: query: ${JSON.stringify(queryParams, null, 4)}`);
const result = await service.indexThreshold.timeSeriesQuery({
const result = await (await data).timeSeriesQuery({
logger,
callCluster,
query: queryParams,

View file

@ -5,9 +5,9 @@
*/
import { ParamsSchema, Params } from './alert_type_params';
import { runTests } from './lib/core_query_types.test';
import { TypeOf } from '@kbn/config-schema';
import { ObjectType, TypeOf } from '@kbn/config-schema';
import type { Writable } from '@kbn/utility-types';
import { CoreQueryParams, MAX_GROUPS } from '../../../../triggers_actions_ui/server';
const DefaultParams: Writable<Partial<Params>> = {
index: 'index-name',
@ -71,3 +71,185 @@ describe('alertType Params validate()', () => {
return ParamsSchema.validate(params);
}
});
export function runTests(schema: ObjectType, defaultTypeParams: Record<string, unknown>): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let params: any;
const CoreDefaultParams: Writable<Partial<CoreQueryParams>> = {
index: 'index-name',
timeField: 'time-field',
aggType: 'count',
groupBy: 'all',
timeWindowSize: 5,
timeWindowUnit: 'm',
};
describe('coreQueryTypes', () => {
beforeEach(() => {
params = { ...CoreDefaultParams, ...defaultTypeParams };
});
it('succeeds with minimal properties', async () => {
expect(validate()).toBeTruthy();
});
it('succeeds with maximal properties', async () => {
params.aggType = 'avg';
params.aggField = 'agg-field';
params.groupBy = 'top';
params.termField = 'group-field';
params.termSize = 200;
expect(validate()).toBeTruthy();
params.index = ['index-name-1', 'index-name-2'];
params.aggType = 'avg';
params.aggField = 'agg-field';
params.groupBy = 'top';
params.termField = 'group-field';
params.termSize = 200;
expect(validate()).toBeTruthy();
});
it('fails for invalid index', async () => {
delete params.index;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[index]: expected at least one defined value but got [undefined]"`
);
params.index = 42;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(`
"[index]: types that failed validation:
- [index.0]: expected value of type [string] but got [number]
- [index.1]: expected value of type [array] but got [number]"
`);
params.index = '';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(`
"[index]: types that failed validation:
- [index.0]: value has length [0] but it must have a minimum length of [1].
- [index.1]: could not parse array value from json input"
`);
params.index = ['', 'a'];
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(`
"[index]: types that failed validation:
- [index.0]: expected value of type [string] but got [Array]
- [index.1.0]: value has length [0] but it must have a minimum length of [1]."
`);
});
it('fails for invalid timeField', async () => {
delete params.timeField;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[timeField]: expected value of type [string] but got [undefined]"`
);
params.timeField = 42;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[timeField]: expected value of type [string] but got [number]"`
);
params.timeField = '';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[timeField]: value has length [0] but it must have a minimum length of [1]."`
);
});
it('fails for invalid aggType', async () => {
params.aggType = 42;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[aggType]: expected value of type [string] but got [number]"`
);
params.aggType = '-not-a-valid-aggType-';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[aggType]: invalid aggType: \\"-not-a-valid-aggType-\\""`
);
});
it('fails for invalid aggField', async () => {
params.aggField = 42;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[aggField]: expected value of type [string] but got [number]"`
);
params.aggField = '';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[aggField]: value has length [0] but it must have a minimum length of [1]."`
);
});
it('fails for invalid termField', async () => {
params.groupBy = 'top';
params.termField = 42;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[termField]: expected value of type [string] but got [number]"`
);
params.termField = '';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[termField]: value has length [0] but it must have a minimum length of [1]."`
);
});
it('fails for invalid termSize', async () => {
params.groupBy = 'top';
params.termField = 'fee';
params.termSize = 'foo';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[termSize]: expected value of type [number] but got [string]"`
);
params.termSize = MAX_GROUPS + 1;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[termSize]: must be less than or equal to 1000"`
);
params.termSize = 0;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[termSize]: Value must be equal to or greater than [1]."`
);
});
it('fails for invalid timeWindowSize', async () => {
params.timeWindowSize = 'foo';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[timeWindowSize]: expected value of type [number] but got [string]"`
);
params.timeWindowSize = 0;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[timeWindowSize]: Value must be equal to or greater than [1]."`
);
});
it('fails for invalid timeWindowUnit', async () => {
params.timeWindowUnit = 42;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[timeWindowUnit]: expected value of type [string] but got [number]"`
);
params.timeWindowUnit = 'x';
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[timeWindowUnit]: invalid timeWindowUnit: \\"x\\""`
);
});
it('fails for invalid aggType/aggField', async () => {
params.aggType = 'avg';
delete params.aggField;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
`"[aggField]: must have a value when [aggType] is \\"avg\\""`
);
});
});
function onValidate(): () => void {
return () => validate();
}
function validate(): unknown {
return schema.validate(params);
}
}

View file

@ -7,7 +7,10 @@
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import { ComparatorFnNames, getInvalidComparatorMessage } from './alert_type';
import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './lib/core_query_types';
import {
CoreQueryParamsSchemaProperties,
validateCoreQueryBody,
} from '../../../../triggers_actions_ui/server';
// alert type parameters

View file

@ -4,34 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Service, AlertingSetup, IRouter } from '../../types';
import { timeSeriesQuery } from './lib/time_series_query';
import { Logger } from 'src/core/server';
import { AlertingSetup, StackAlertsStartDeps } from '../../types';
import { getAlertType } from './alert_type';
import { registerRoutes } from './routes';
// future enhancement: make these configurable?
export const MAX_INTERVALS = 1000;
export const MAX_GROUPS = 1000;
export const DEFAULT_GROUPS = 100;
export function getService() {
return {
timeSeriesQuery,
};
}
interface RegisterParams {
service: Service;
router: IRouter;
logger: Logger;
data: Promise<StackAlertsStartDeps['triggersActionsUi']['data']>;
alerts: AlertingSetup;
baseRoute: string;
}
export function register(params: RegisterParams) {
const { service, router, alerts, baseRoute } = params;
alerts.registerType(getAlertType(service));
const baseBuiltInRoute = `${baseRoute}/index_threshold`;
registerRoutes({ service, router, baseRoute: baseBuiltInRoute });
const { logger, data, alerts } = params;
alerts.registerType(getAlertType(logger, data));
}

View file

@ -4,15 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server';
import { AlertingBuiltinsPlugin } from './plugin';
import { configSchema } from './config';
import { configSchema, Config } from '../common/config';
export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type';
export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx);
export const config = {
export const config: PluginConfigDescriptor<Config> = {
exposeToBrowser: {
enableGeoTrackingThresholdAlert: true,
},
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
renameFromRoot(
'xpack.triggers_actions_ui.enableGeoTrackingThresholdAlert',
'xpack.stack_alerts.enableGeoTrackingThresholdAlert'
),
],
};
export { IService } from './types';
export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx);

View file

@ -69,34 +69,5 @@ describe('AlertingBuiltins Plugin', () => {
expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE);
});
it('should return a service in the expected shape', async () => {
const alertingSetup = alertsMock.createSetup();
const featuresSetup = featuresPluginMock.createSetup();
const service = await plugin.setup(coreSetup, {
alerts: alertingSetup,
features: featuresSetup,
});
expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function');
});
});
describe('start()', () => {
let context: ReturnType<typeof coreMock['createPluginInitializerContext']>;
let plugin: AlertingBuiltinsPlugin;
let coreStart: ReturnType<typeof coreMock['createStart']>;
beforeEach(() => {
context = coreMock.createPluginInitializerContext();
plugin = new AlertingBuiltinsPlugin(context);
coreStart = coreMock.createStart();
});
it('should return a service in the expected shape', async () => {
const service = await plugin.start(coreStart);
expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function');
});
});
});

View file

@ -4,40 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/server';
import { Plugin, Logger, CoreSetup, PluginInitializerContext } from 'src/core/server';
import { Service, IService, StackAlertsDeps } from './types';
import { getService as getServiceIndexThreshold } from './alert_types/index_threshold';
import { StackAlertsDeps, StackAlertsStartDeps } from './types';
import { registerBuiltInAlertTypes } from './alert_types';
import { BUILT_IN_ALERTS_FEATURE } from './feature';
export class AlertingBuiltinsPlugin implements Plugin<IService, IService> {
export class AlertingBuiltinsPlugin
implements Plugin<void, void, StackAlertsDeps, StackAlertsStartDeps> {
private readonly logger: Logger;
private readonly service: Service;
constructor(ctx: PluginInitializerContext) {
this.logger = ctx.logger.get();
this.service = {
indexThreshold: getServiceIndexThreshold(),
logger: this.logger,
};
}
public async setup(core: CoreSetup, { alerts, features }: StackAlertsDeps): Promise<IService> {
public async setup(
core: CoreSetup<StackAlertsStartDeps>,
{ alerts, features }: StackAlertsDeps
): Promise<void> {
features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE);
registerBuiltInAlertTypes({
service: this.service,
router: core.http.createRouter(),
logger: this.logger,
data: core
.getStartServices()
.then(async ([, { triggersActionsUi }]) => triggersActionsUi.data),
alerts,
baseRoute: '/api/stack_alerts',
});
return this.service;
}
public async start(core: CoreStart): Promise<IService> {
return this.service;
}
public async start(): Promise<void> {}
public async stop(): Promise<void> {}
}

View file

@ -4,11 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger, LegacyScopedClusterClient } from '../../../../src/core/server';
import { PluginStartContract as TriggersActionsUiStartContract } from '../../triggers_actions_ui/server';
import { PluginSetupContract as AlertingSetup } from '../../alerts/server';
import { getService as getServiceIndexThreshold } from './alert_types/index_threshold';
export { Logger, IRouter } from '../../../../src/core/server';
export {
PluginSetupContract as AlertingSetup,
@ -23,14 +20,6 @@ export interface StackAlertsDeps {
features: FeaturesPluginSetup;
}
// external service exposed through plugin setup/start
export interface IService {
indexThreshold: ReturnType<typeof getServiceIndexThreshold>;
export interface StackAlertsStartDeps {
triggersActionsUi: TriggersActionsUiStartContract;
}
// version of service for internal use
export interface Service extends IService {
logger: Logger;
}
export type CallCluster = LegacyScopedClusterClient['callAsCurrentUser'];

View file

@ -19525,24 +19525,24 @@
"xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "しきい値として使用する値の配列。「between」と「notBetween」には2つの値が必要です。その他は1つの値が必要です。",
"xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "アラートの事前構成タイトル。",
"xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "しきい値を超えた値。",
"xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です",
"xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です",
"xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "アラート{name}グループ{group}値{value}が{date}に{window}にわたってしきい値{function}を超えました",
"xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "アラート{name}グループ{group}がしきい値を超えました",
"xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値",
"xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です",
"xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」",
"xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります",
"xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」",
"xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です",
"xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」",
"xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります",
"xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」",
"xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効なthresholdComparatorが指定されました: {comparator}",
"xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "無効な日付{date}",
"xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "無効な期間:「{duration}」",
"xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」",
"xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。",
"xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "無効な日付{date}",
"xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "無効な期間:「{duration}」",
"xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」",
"xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。",
"xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には2つの要素が必要です",
"xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」",
"xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です",
"xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です",
"xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です",
"xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」",
"xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です",
"xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です",
"xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です",
"xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除",
"xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除",
"xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除",
@ -20078,42 +20078,42 @@
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル",
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ",
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "{numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}を回復できません。",
"xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "境界名を選択",
"xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)",
"xpack.triggersActionsUI.geoThreshold.delayOffset": "遅延評価オフセット",
"xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します",
"xpack.triggersActionsUI.geoThreshold.entityByLabel": "グループ基準",
"xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "インデックス",
"xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。",
"xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。",
"xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。",
"xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。",
"xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "エンティティは必須です。",
"xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。",
"xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。",
"xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。",
"xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。",
"xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空間フィールド",
"xpack.triggersActionsUI.geoThreshold.indexLabel": "インデックス",
"xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "インデックスパターン",
"xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択",
"xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "追跡しきい値",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした",
"xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "境界を選択:",
"xpack.triggersActionsUI.geoThreshold.selectEntity": "エンティティを選択",
"xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "ジオフィールドを選択",
"xpack.triggersActionsUI.geoThreshold.selectIndex": "条件を定義してください",
"xpack.triggersActionsUI.geoThreshold.selectLabel": "ジオフィールドを選択",
"xpack.triggersActionsUI.geoThreshold.selectOffset": "オフセットを選択(任意)",
"xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "時刻フィールドを選択",
"xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "時間フィールド",
"xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択",
"xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "エンティティ",
"xpack.stackAlerts.geoThreshold.boundaryNameSelect": "境界名を選択",
"xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)",
"xpack.stackAlerts.geoThreshold.delayOffset": "遅延評価オフセット",
"xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します",
"xpack.stackAlerts.geoThreshold.entityByLabel": "グループ基準",
"xpack.stackAlerts.geoThreshold.entityIndexLabel": "インデックス",
"xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。",
"xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。",
"xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。",
"xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。",
"xpack.stackAlerts.geoThreshold.error.requiredEntityText": "エンティティは必須です。",
"xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。",
"xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。",
"xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。",
"xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。",
"xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空間フィールド",
"xpack.stackAlerts.geoThreshold.indexLabel": "インデックス",
"xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "インデックスパターン",
"xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択",
"xpack.stackAlerts.geoThreshold.name.trackingThreshold": "追跡しきい値",
"xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します",
"xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ",
"xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む",
"xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。",
"xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ",
"xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした",
"xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "境界を選択:",
"xpack.stackAlerts.geoThreshold.selectEntity": "エンティティを選択",
"xpack.stackAlerts.geoThreshold.selectGeoLabel": "ジオフィールドを選択",
"xpack.stackAlerts.geoThreshold.selectIndex": "条件を定義してください",
"xpack.stackAlerts.geoThreshold.selectLabel": "ジオフィールドを選択",
"xpack.stackAlerts.geoThreshold.selectOffset": "オフセットを選択(任意)",
"xpack.stackAlerts.geoThreshold.selectTimeLabel": "時刻フィールドを選択",
"xpack.stackAlerts.geoThreshold.timeFieldLabel": "時間フィールド",
"xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択",
"xpack.stackAlerts.geoThreshold.whenEntityLabel": "エンティティ",
"xpack.triggersActionsUI.home.alertsTabTitle": "アラート",
"xpack.triggersActionsUI.home.appTitle": "アラートとアクション",
"xpack.triggersActionsUI.home.breadcrumbTitle": "アラートとアクション",
@ -20156,15 +20156,15 @@
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "値が必要です。",
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "メソッドが必要です",
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "パスワードが必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText": "集約フィールドが必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText": "インデックスが必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText": "用語サイズが必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text": "しきい値 0 が必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text": "しきい値 1 が必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "時間フィールドが必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "用語フィールドが必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "集約フィールドが必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "インデックスが必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "用語サイズが必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "しきい値 0 が必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "しきい値 1 が必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "時間フィールドが必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "用語フィールドが必要です。",
"xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} コネクタ",
"xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択",
"xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "コネクターを作成できません。",
@ -20173,27 +20173,27 @@
"xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} コネクター",
"xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存",
"xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました",
"xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "条件を定義してください",
"xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません",
"xpack.stackAlerts.threshold.ui.conditionPrompt": "条件を定義してください",
"xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません",
"xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "アラートの作成",
"xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "閉じる",
"xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...",
"xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "閉じる",
"xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...",
"xpack.triggersActionsUI.sections.alertAdd.operationName": "作成",
"xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。",
"xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。",
"xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。",
"xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました",
"xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください",
"xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "閉じる",
"xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。",
"xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。",
"xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel": "インデックス",
"xpack.triggersActionsUI.sections.alertAdd.threshold.indexLabel": "インデックス",
"xpack.triggersActionsUI.sections.alertAdd.threshold.indicesToQueryLabel": "クエリを実行するインデックス",
"xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldLabel": "時間フィールド",
"xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel": "フィールドを選択",
"xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。",
"xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません",
"xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません",
"xpack.stackAlerts.threshold.ui.selectIndex": "インデックスを選択してください",
"xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "閉じる",
"xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。",
"xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。",
"xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "インデックス",
"xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "インデックス",
"xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "クエリを実行するインデックス",
"xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "時間フィールド",
"xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "フィールドを選択",
"xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。",
"xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません",
"xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません",
"xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "このアラートは無効になっていて再表示できません。[↑ を無効にする]を切り替えてアクティブにします。",
"xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "期間",
"xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "インスタンス",

View file

@ -19544,24 +19544,24 @@
"xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "用作阈值的值数组“between”和“notBetween”需要两个值其他则需要一个值。",
"xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "告警的预构造标题。",
"xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "超过阈值的值。",
"xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值",
"xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值",
"xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "告警 {name} 组 {group} 值 {value} 在 {window} 于 {date}超过了阈值 {function}",
"xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "告警 {name} 组 {group} 超过了阈值",
"xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值",
"xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]",
"xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”",
"xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定",
"xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”",
"xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]",
"xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”",
"xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定",
"xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”",
"xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}",
"xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "日期 {date} 无效",
"xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "持续时间无效:“{duration}”",
"xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”",
"xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}",
"xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "日期 {date} 无效",
"xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "持续时间无效:“{duration}”",
"xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”",
"xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}",
"xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素",
"xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”",
"xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}",
"xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField][groupBy] 为 top 时termField 为必需",
"xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize][groupBy] 为 top 时termSize 为必需",
"xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”",
"xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}",
"xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField][groupBy] 为 top 时termField 为必需",
"xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize][groupBy] 为 top 时termSize 为必需",
"xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引",
"xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式",
"xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}",
@ -20097,42 +20097,42 @@
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消",
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ",
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "无法恢复{numIdsToDelete, plural, one {删除的{singleTitle}} other {删除的{multipleTitle}}}。",
"xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "选择边界名称",
"xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)",
"xpack.triggersActionsUI.geoThreshold.delayOffset": "已延迟的评估偏移",
"xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整",
"xpack.triggersActionsUI.geoThreshold.entityByLabel": "方式",
"xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "索引",
"xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。",
"xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。",
"xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。",
"xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。",
"xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "“实体”必填。",
"xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。",
"xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。",
"xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。",
"xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。",
"xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空间字段",
"xpack.triggersActionsUI.geoThreshold.indexLabel": "索引",
"xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "索引模式",
"xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式",
"xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "跟踪阈值",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ",
"xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式",
"xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "选择边界:",
"xpack.triggersActionsUI.geoThreshold.selectEntity": "选择实体",
"xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "选择地理字段",
"xpack.triggersActionsUI.geoThreshold.selectIndex": "定义条件",
"xpack.triggersActionsUI.geoThreshold.selectLabel": "选择地理字段",
"xpack.triggersActionsUI.geoThreshold.selectOffset": "选择偏移(可选)",
"xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "选择时间字段",
"xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "时间字段",
"xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段",
"xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "当实体",
"xpack.stackAlerts.geoThreshold.boundaryNameSelect": "选择边界名称",
"xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)",
"xpack.stackAlerts.geoThreshold.delayOffset": "已延迟的评估偏移",
"xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整",
"xpack.stackAlerts.geoThreshold.entityByLabel": "方式",
"xpack.stackAlerts.geoThreshold.entityIndexLabel": "索引",
"xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。",
"xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。",
"xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。",
"xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。",
"xpack.stackAlerts.geoThreshold.error.requiredEntityText": "“实体”必填。",
"xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。",
"xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。",
"xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。",
"xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。",
"xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空间字段",
"xpack.stackAlerts.geoThreshold.indexLabel": "索引",
"xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "索引模式",
"xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式",
"xpack.stackAlerts.geoThreshold.name.trackingThreshold": "跟踪阈值",
"xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式",
"xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ",
"xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。",
"xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。",
"xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ",
"xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式",
"xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "选择边界:",
"xpack.stackAlerts.geoThreshold.selectEntity": "选择实体",
"xpack.stackAlerts.geoThreshold.selectGeoLabel": "选择地理字段",
"xpack.stackAlerts.geoThreshold.selectIndex": "定义条件",
"xpack.stackAlerts.geoThreshold.selectLabel": "选择地理字段",
"xpack.stackAlerts.geoThreshold.selectOffset": "选择偏移(可选)",
"xpack.stackAlerts.geoThreshold.selectTimeLabel": "选择时间字段",
"xpack.stackAlerts.geoThreshold.timeFieldLabel": "时间字段",
"xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段",
"xpack.stackAlerts.geoThreshold.whenEntityLabel": "当实体",
"xpack.triggersActionsUI.home.alertsTabTitle": "告警",
"xpack.triggersActionsUI.home.appTitle": "告警和操作",
"xpack.triggersActionsUI.home.breadcrumbTitle": "告警和操作",
@ -20176,15 +20176,15 @@
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "“值”必填。",
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "“方法”必填",
"xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "“密码”必填。",
"xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText": "聚合字段必填。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText": "“索引”必填。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText": "“词大小”必填。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text": "阈值 0 必填。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text": "阈值 1 必填。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "时间字段必填。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "“时间窗大小”必填。",
"xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "词字段必填。",
"xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "聚合字段必填。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "“索引”必填。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "“词大小”必填。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "阈值 0 必填。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "阈值 1 必填。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "时间字段必填。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "“时间窗大小”必填。",
"xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "词字段必填。",
"xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器",
"xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器",
"xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。",
@ -20193,27 +20193,27 @@
"xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器",
"xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存",
"xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”",
"xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件",
"xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化",
"xpack.stackAlerts.threshold.ui.conditionPrompt": "定义条件",
"xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "无法加载告警可视化",
"xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警",
"xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "关闭",
"xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "正在加载告警可视化……",
"xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "关闭",
"xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "正在加载告警可视化……",
"xpack.triggersActionsUI.sections.alertAdd.operationName": "创建",
"xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "完成表达式以生成预览。",
"xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "完成表达式以生成预览。",
"xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。",
"xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”",
"xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引",
"xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "关闭",
"xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。",
"xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。",
"xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel": "索引",
"xpack.triggersActionsUI.sections.alertAdd.threshold.indexLabel": "索引",
"xpack.triggersActionsUI.sections.alertAdd.threshold.indicesToQueryLabel": "要查询的索引",
"xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldLabel": "时间字段",
"xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel": "选择字段",
"xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。",
"xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询",
"xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "无法加载可视化",
"xpack.stackAlerts.threshold.ui.selectIndex": "选择索引",
"xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "关闭",
"xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。",
"xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。",
"xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "索引",
"xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "索引",
"xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "要查询的索引",
"xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "时间字段",
"xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "选择字段",
"xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。",
"xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询",
"xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "无法加载可视化",
"xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "此告警已禁用,无法显示。切换禁用 ↑ 以激活。",
"xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "持续时间",
"xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "实例",

View file

@ -220,7 +220,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr
<EuiFormLabel>
<FormattedMessage
defaultMessage="Select Index to query:"
id="xpack.triggersActionsUI.sections.alertAdd.selectIndex"
id="xpack.stackAlerts.threshold.ui.selectIndex"
/>
....
</Fragment>

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './data';

View file

@ -3,8 +3,8 @@
"version": "kibana",
"server": true,
"ui": true,
"optionalPlugins": ["alerts", "stackAlerts", "features", "home"],
"requiredPlugins": ["management", "charts", "data", "kibanaReact"],
"optionalPlugins": ["alerts", "features", "home"],
"requiredPlugins": ["management", "charts", "data"],
"configPath": ["xpack", "trigger_actions_ui"],
"extraPublicDirs": ["public/common", "public/common/constants"],
"requiredBundles": ["home", "alerts", "esUiShared"]

View file

@ -9,7 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { SavedObjectsClientContract } from 'src/core/public';
import { App, AppDeps } from './app';
import { setSavedObjectsClient } from '../common/lib/index_threshold_api';
import { setSavedObjectsClient } from '../common/lib/data_apis';
interface BootDeps extends AppDeps {
element: HTMLElement;

View file

@ -5,6 +5,10 @@
*/
export * from './expression_items';
export * from './constants';
export * from './index_controls';
export * from './lib';
export * from './types';
export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config';
export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config';

View file

@ -9,10 +9,10 @@ import { HttpSetup } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import {
loadIndexPatterns,
getMatchingIndicesForThresholdAlertType,
getThresholdAlertTypeFields,
getMatchingIndices,
getESIndexFields,
getSavedObjectsClient,
} from '../lib/index_threshold_api';
} from '../lib/data_apis';
export interface IOption {
label: string;
@ -39,7 +39,7 @@ export const getIndexOptions = async (
return options;
}
const matchingIndices = (await getMatchingIndicesForThresholdAlertType({
const matchingIndices = (await getMatchingIndices({
pattern,
http,
})) as string[];
@ -85,12 +85,15 @@ export const getIndexOptions = async (
};
export const getFields = async (http: HttpSetup, indexes: string[]) => {
return await getThresholdAlertTypeFields({ indexes, http });
return await getESIndexFields({ indexes, http });
};
export const firstFieldOption = {
text: i18n.translate('xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel', {
defaultMessage: 'Select a field',
}),
text: i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel',
{
defaultMessage: 'Select a field',
}
),
value: '',
};

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup } from 'kibana/public';
const DATA_API_ROOT = '/api/triggers_actions_ui/data';
export async function getMatchingIndices({
pattern,
http,
}: {
pattern: string;
http: HttpSetup;
}): Promise<Record<string, any>> {
if (!pattern.startsWith('*')) {
pattern = `*${pattern}`;
}
if (!pattern.endsWith('*')) {
pattern = `${pattern}*`;
}
const { indices } = await http.post(`${DATA_API_ROOT}/_indices`, {
body: JSON.stringify({ pattern }),
});
return indices;
}
export async function getESIndexFields({
indexes,
http,
}: {
indexes: string[];
http: HttpSetup;
}): Promise<
Array<{
name: string;
type: string;
normalizedType: string;
searchable: boolean;
aggregatable: boolean;
}>
> {
const { fields } = await http.post(`${DATA_API_ROOT}/_fields`, {
body: JSON.stringify({ indexPatterns: indexes }),
});
return fields;
}
let savedObjectsClient: any;
export const setSavedObjectsClient = (aSavedObjectsClient: any) => {
savedObjectsClient = aSavedObjectsClient;
};
export const getSavedObjectsClient = () => {
return savedObjectsClient;
};
export const loadIndexPatterns = async () => {
const { savedObjects } = await getSavedObjectsClient().find({
type: 'index-pattern',
fields: ['title'],
perPage: 10000,
});
return savedObjects;
};

View file

@ -0,0 +1,6 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { getTimeFieldOptions, getTimeOptions } from './get_time_options';

View file

@ -1,91 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup } from 'kibana/public';
import { TimeSeriesResult } from '../../../../stack_alerts/common/alert_types/index_threshold';
const INDEX_THRESHOLD_API_ROOT = '/api/stack_alerts/index_threshold';
export async function getMatchingIndicesForThresholdAlertType({
pattern,
http,
}: {
pattern: string;
http: HttpSetup;
}): Promise<Record<string, any>> {
if (!pattern.startsWith('*')) {
pattern = `*${pattern}`;
}
if (!pattern.endsWith('*')) {
pattern = `${pattern}*`;
}
const { indices } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_indices`, {
body: JSON.stringify({ pattern }),
});
return indices;
}
export async function getThresholdAlertTypeFields({
indexes,
http,
}: {
indexes: string[];
http: HttpSetup;
}): Promise<Record<string, any>> {
const { fields } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_fields`, {
body: JSON.stringify({ indexPatterns: indexes }),
});
return fields;
}
let savedObjectsClient: any;
export const setSavedObjectsClient = (aSavedObjectsClient: any) => {
savedObjectsClient = aSavedObjectsClient;
};
export const getSavedObjectsClient = () => {
return savedObjectsClient;
};
export const loadIndexPatterns = async () => {
const { savedObjects } = await getSavedObjectsClient().find({
type: 'index-pattern',
fields: ['title'],
perPage: 10000,
});
return savedObjects;
};
interface GetThresholdAlertVisualizationDataParams {
model: any;
visualizeOptions: any;
http: HttpSetup;
}
export async function getThresholdAlertVisualizationData({
model,
visualizeOptions,
http,
}: GetThresholdAlertVisualizationDataParams): Promise<TimeSeriesResult> {
const timeSeriesQueryParams = {
index: model.index,
timeField: model.timeField,
aggType: model.aggType,
aggField: model.aggField,
groupBy: model.groupBy,
termField: model.termField,
termSize: model.termSize,
timeWindowSize: model.timeWindowSize,
timeWindowUnit: model.timeWindowUnit,
dateStart: new Date(visualizeOptions.rangeFrom).toISOString(),
dateEnd: new Date(visualizeOptions.rangeTo).toISOString(),
interval: visualizeOptions.interval,
};
return await http.post<TimeSeriesResult>(`${INDEX_THRESHOLD_API_ROOT}/_time_series_query`, {
body: JSON.stringify(timeSeriesQueryParams),
});
}

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/public';
import { Plugin } from './plugin';
export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context';
@ -22,15 +21,17 @@ export {
ValidationResult,
ActionVariable,
ActionConnector,
IErrorObject,
} from './types';
export {
ConnectorAddFlyout,
ConnectorEditFlyout,
} from './application/sections/action_connector_form';
export { loadActionTypes } from './application/lib/action_connector_api';
export * from './common';
export function plugin(ctx: PluginInitializerContext) {
return new Plugin(ctx);
export function plugin() {
return new Plugin();
}
export { Plugin };

View file

@ -4,17 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
CoreSetup,
CoreStart,
Plugin as CorePlugin,
PluginInitializerContext,
} from 'src/core/public';
import { CoreSetup, CoreStart, Plugin as CorePlugin } from 'src/core/public';
import { i18n } from '@kbn/i18n';
import { FeaturesPluginStart } from '../../features/public';
import { registerBuiltInActionTypes } from './application/components/builtin_action_types';
import { registerBuiltInAlertTypes } from './application/components/builtin_alert_types';
import { ActionTypeModel, AlertTypeModel } from './types';
import { TypeRegistry } from './application/type_registry';
import {
@ -29,10 +23,6 @@ import { ChartsPluginStart } from '../../../../src/plugins/charts/public';
import { PluginStartContract as AlertingStart } from '../../alerts/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
export interface TriggersActionsUiConfigType {
enableGeoTrackingThresholdAlert: boolean;
}
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
@ -66,14 +56,10 @@ export class Plugin
> {
private actionTypeRegistry: TypeRegistry<ActionTypeModel>;
private alertTypeRegistry: TypeRegistry<AlertTypeModel>;
private initializerContext: PluginInitializerContext;
constructor(initializerContext: PluginInitializerContext) {
constructor() {
this.actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
this.alertTypeRegistry = new TypeRegistry<AlertTypeModel>();
this.initializerContext = initializerContext;
}
public setup(core: CoreSetup, plugins: PluginsSetup): TriggersAndActionsUIPublicPluginSetup {
@ -142,11 +128,6 @@ export class Plugin
actionTypeRegistry: this.actionTypeRegistry,
});
registerBuiltInAlertTypes({
alertTypeRegistry: this.alertTypeRegistry,
triggerActionsUiConfig: this.initializerContext.config.get<TriggersActionsUiConfigType>(),
});
return {
actionTypeRegistry: this.actionTypeRegistry,
alertTypeRegistry: this.alertTypeRegistry,

View file

@ -0,0 +1,228 @@
# Data Apis
The TriggersActionsUi plugin's Data Apis back the functionality needed by the Index Threshold Stack Alert.
## http endpoints
The following endpoints are provided for this alert type:
- `POST /api/triggers_actions_ui/data/_indices`
- `POST /api/triggers_actions_ui/data/_fields`
- `POST /api/triggers_actions_ui/data/_time_series_query`
### `POST .../_indices`
This HTTP endpoint is provided for the alerting ui to list the available
"index names" for the user to select to use with the alert. This API also
returns aliases which match the supplied pattern.
The request body is expected to be a JSON object in the following form, where the
`pattern` value may include comma-separated names and wildcards.
```js
{
pattern: "index-name-pattern"
}
```
The response body is a JSON object in the following form, where each element
of the `indices` array is the name of an index or alias. The number of elements
returned is limited, as this API is intended to be used to help narrow down
index names to use with the alert, and not support pagination, etc.
```js
{
indices: ["index-name-1", "alias-name-1", ...]
}
```
### `POST .../_fields`
This HTTP endpoint is provided for the alerting ui to list the available
fields for the user to select to use with the alert.
The request body is expected to be a JSON object in the following form, where the
`indexPatterns` array elements may include comma-separated names and wildcards.
```js
{
indexPatterns: ["index-pattern-1", "index-pattern-2"]
}
```
The response body is a JSON object in the following form, where each element
fields array is a field object.
```js
{
fields: [fieldObject1, fieldObject2, ...]
}
```
A field object is the following shape:
```typescript
{
name: string, // field name
type: string, // field type - eg 'keyword', 'date', 'long', etc
normalizedType: string, // for numeric types, this will be 'number'
aggregatable: true, // value from elasticsearch field capabilities
searchable: true, // value from elasticsearch field capabilities
}
```
### `POST .../_time_series_query`
This HTTP endpoint is provided to return the values the alertType would calculate,
over a series of time. It is intended to be used in the alerting UI to
provide a "preview" of the alert during creation/editing based on recent data,
and could be used to show a "simulation" of the the alert over an arbitrary
range of time.
The endpoint is `POST /api/triggers_actions_ui/data/_time_series_query`.
The request and response bodies are specifed in
[`lib/core_query_types.ts`][it-core-query]
and
[`lib/time_series_types.ts`][it-timeSeries-types].
The request body is very similar to the alertType's parameters.
### example
Continuing with the example above, here's a query to get the values calculated
for the last 10 seconds.
This example uses [now-iso][] to generate iso date strings.
```console
curl -k "https://elastic:changeme@localhost:5601/api/triggers_actions_ui/data/_time_series_query" \
-H "kbn-xsrf: foo" -H "content-type: application/json" -d "{
\"index\": \"es-hb-sim\",
\"timeField\": \"@timestamp\",
\"aggType\": \"avg\",
\"aggField\": \"summary.up\",
\"groupBy\": \"top\",
\"termSize\": 100,
\"termField\": \"monitor.name.keyword\",
\"interval\": \"1s\",
\"dateStart\": \"`now-iso -10s`\",
\"dateEnd\": \"`now-iso`\",
\"timeWindowSize\": 5,
\"timeWindowUnit\": \"s\"
}"
```
```
{
"results": [
{
"group": "host-A",
"metrics": [
[ "2020-02-26T15:10:40.000Z", 0 ],
[ "2020-02-26T15:10:41.000Z", 0 ],
[ "2020-02-26T15:10:42.000Z", 0 ],
[ "2020-02-26T15:10:43.000Z", 0 ],
[ "2020-02-26T15:10:44.000Z", 0 ],
[ "2020-02-26T15:10:45.000Z", 0 ],
[ "2020-02-26T15:10:46.000Z", 0 ],
[ "2020-02-26T15:10:47.000Z", 0 ],
[ "2020-02-26T15:10:48.000Z", 0 ],
[ "2020-02-26T15:10:49.000Z", 0 ],
[ "2020-02-26T15:10:50.000Z", 0 ]
]
}
]
}
```
To get the current value of the calculated metric, you can leave off the date:
```
curl -k "https://elastic:changeme@localhost:5601/api/triggers_actions_ui/data/_time_series_query" \
-H "kbn-xsrf: foo" -H "content-type: application/json" -d '{
"index": "es-hb-sim",
"timeField": "@timestamp",
"aggType": "avg",
"aggField": "summary.up",
"groupBy": "top",
"termField": "monitor.name.keyword",
"termSize": 100,
"interval": "1s",
"timeWindowSize": 5,
"timeWindowUnit": "s"
}'
```
```
{
"results": [
{
"group": "host-A",
"metrics": [
[ "2020-02-26T15:23:36.635Z", 0 ]
]
}
]
}
```
[it-timeSeries-types]: lib/time_series_types.ts
## service functions
A single service function is available that provides the functionality
of the http endpoint `POST /api/triggers_actions_ui/data/_time_series_query`,
but as an API for Kibana plugins. The function is available as
`triggersActionsUi.data.timeSeriesQuery()` on the plugin's _Start_ contract
The parameters and return value for the function are the same as for the HTTP
request, though some additional parameters are required (logger, callCluster,
etc).
## notes on the timeSeriesQuery API / http endpoint
This API provides additional parameters beyond what the alertType itself uses:
- `dateStart`
- `dateEnd`
- `interval`
The `dateStart` and `dateEnd` parameters are ISO date strings.
The `interval` parameter is intended to model the `interval` the alert is
currently using, and uses the same `1s`, `2m`, `3h`, etc format. Over the
supplied date range, a time-series data point will be calculated every
`interval` duration.
So the number of time-series points in the output of the API should be:
```
( dateStart - dateEnd ) / interval
```
Example:
```
dateStart: '2020-01-01T00:00:00'
dateEnd: '2020-01-02T00:00:00'
interval: '1h'
```
The date range is 1 day === 24 hours. The interval is 1 hour. So there should
be ~24 time series points in the output.
For preview purposes:
- The `termSize` parameter should be used to help cut
down on the amount of work ES does, and keep the generated graphs a little
simpler. Probably something like `10`.
- For queries with long date ranges, you probably don't want to use the
`interval` the alert is set to, as the `interval` used in the query, as this
could result in a lot of time-series points being generated, which is both
costly in ES, and may result in noisy graphs.
- The `timeWindow*` parameters should be the same as what the alert is using,
especially for the `count` and `sum` aggregation types. Those aggregations
don't scale the same way the others do, when the window changes. Even for
the other aggregations, changing the window could result in dramatically
different values being generated - `avg` will be more "average-y", `min`
and `max` will be a little stickier.

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger, IRouter } from '../../../../../src/core/server';
import { timeSeriesQuery } from './lib/time_series_query';
import { registerRoutes } from './routes';
export {
TimeSeriesQuery,
CoreQueryParams,
CoreQueryParamsSchemaProperties,
validateCoreQueryBody,
} from './lib';
// future enhancement: make these configurable?
export const MAX_INTERVALS = 1000;
export const MAX_GROUPS = 1000;
export const DEFAULT_GROUPS = 100;
export function getService() {
return {
timeSeriesQuery,
};
}
interface RegisterParams {
logger: Logger;
router: IRouter;
data: ReturnType<typeof getService>;
baseRoute: string;
}
export function register(params: RegisterParams) {
const { logger, router, data, baseRoute } = params;
const baseBuiltInRoute = `${baseRoute}/data`;
registerRoutes({ logger, router, data, baseRoute: baseBuiltInRoute });
}

View file

@ -51,33 +51,45 @@ export function validateCoreQueryBody(anyParams: unknown): string | undefined {
termSize,
}: CoreQueryParams = anyParams as CoreQueryParams;
if (aggType !== 'count' && !aggField) {
return i18n.translate('xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage', {
defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"',
values: {
aggType,
},
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage',
{
defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"',
values: {
aggType,
},
}
);
}
// check grouping
if (groupBy === 'top') {
if (termField == null) {
return i18n.translate('xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage', {
defaultMessage: '[termField]: termField required when [groupBy] is top',
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage',
{
defaultMessage: '[termField]: termField required when [groupBy] is top',
}
);
}
if (termSize == null) {
return i18n.translate('xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage', {
defaultMessage: '[termSize]: termSize required when [groupBy] is top',
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage',
{
defaultMessage: '[termSize]: termSize required when [groupBy] is top',
}
);
}
if (termSize > MAX_GROUPS) {
return i18n.translate('xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage', {
defaultMessage: '[termSize]: must be less than or equal to {maxGroups}',
values: {
maxGroups: MAX_GROUPS,
},
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage',
{
defaultMessage: '[termSize]: must be less than or equal to {maxGroups}',
values: {
maxGroups: MAX_GROUPS,
},
}
);
}
}
}
@ -89,7 +101,7 @@ function validateAggType(aggType: string): string | undefined {
return;
}
return i18n.translate('xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage', {
return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage', {
defaultMessage: 'invalid aggType: "{aggType}"',
values: {
aggType,
@ -102,7 +114,7 @@ export function validateGroupBy(groupBy: string): string | undefined {
return;
}
return i18n.translate('xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage', {
return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage', {
defaultMessage: 'invalid groupBy: "{groupBy}"',
values: {
groupBy,
@ -117,10 +129,13 @@ export function validateTimeWindowUnits(timeWindowUnit: string): string | undefi
return;
}
return i18n.translate('xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage', {
defaultMessage: 'invalid timeWindowUnit: "{timeWindowUnit}"',
values: {
timeWindowUnit,
},
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage',
{
defaultMessage: 'invalid timeWindowUnit: "{timeWindowUnit}"',
values: {
timeWindowUnit,
},
}
);
}

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { times } from 'lodash';
import { parseDuration } from '../../../../../alerts/server';
import { parseDuration } from '../../../../alerts/server';
import { MAX_INTERVALS } from '../index';
// dates as numbers are epoch millis
@ -100,7 +100,7 @@ function getDuration(durationS: string, field: string): number {
}
function getParseErrorMessage(formatName: string, fieldName: string, fieldValue: string) {
return i18n.translate('xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage', {
return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage', {
defaultMessage: 'invalid {formatName} format for {fieldName}: "{fieldValue}"',
values: {
formatName,
@ -111,7 +111,7 @@ function getParseErrorMessage(formatName: string, fieldName: string, fieldValue:
}
export function getTooManyIntervalsErrorMessage(intervals: number, maxIntervals: number) {
return i18n.translate('xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage', {
return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage', {
defaultMessage:
'calculated number of intervals {intervals} is greater than maximum {maxIntervals}',
values: {
@ -122,7 +122,10 @@ export function getTooManyIntervalsErrorMessage(intervals: number, maxIntervals:
}
export function getDateStartAfterDateEndErrorMessage(): string {
return i18n.translate('xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage', {
defaultMessage: '[dateStart]: is greater than [dateEnd]',
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage',
{
defaultMessage: '[dateStart]: is greater than [dateEnd]',
}
);
}

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { TimeSeriesQuery } from './time_series_query';
export {
CoreQueryParams,
CoreQueryParamsSchemaProperties,
validateCoreQueryBody,
} from './core_query_types';

View file

@ -6,12 +6,8 @@
// test error conditions of calling timeSeriesQuery - postive results tested in FT
import { loggingSystemMock } from '../../../../../../../src/core/server/mocks';
import { coreMock } from '../../../../../../../src/core/server/mocks';
import { AlertingBuiltinsPlugin } from '../../../plugin';
import { TimeSeriesQueryParameters, TimeSeriesResult, TimeSeriesQuery } from './time_series_query';
type TimeSeriesQueryFn = (query: TimeSeriesQueryParameters) => Promise<TimeSeriesResult>;
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { TimeSeriesQueryParameters, TimeSeriesQuery, timeSeriesQuery } from './time_series_query';
const DefaultQueryParams: TimeSeriesQuery = {
index: 'index-name',
@ -32,16 +28,7 @@ describe('timeSeriesQuery', () => {
let params: TimeSeriesQueryParameters;
const mockCallCluster = jest.fn();
let timeSeriesQueryFn: TimeSeriesQueryFn;
beforeEach(async () => {
// rather than use the function from an import, retrieve it from the plugin
const context = coreMock.createPluginInitializerContext();
const plugin = new AlertingBuiltinsPlugin(context);
const coreStart = coreMock.createStart();
const service = await plugin.start(coreStart);
timeSeriesQueryFn = service.indexThreshold.timeSeriesQuery;
mockCallCluster.mockReset();
params = {
logger: loggingSystemMock.create().get(),
@ -52,14 +39,14 @@ describe('timeSeriesQuery', () => {
it('fails as expected when the callCluster call fails', async () => {
mockCallCluster.mockRejectedValue(new Error('woopsie'));
expect(timeSeriesQueryFn(params)).rejects.toThrowErrorMatchingInlineSnapshot(
expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot(
`"error running search"`
);
});
it('fails as expected when the query params are invalid', async () => {
params.query = { ...params.query, dateStart: 'x' };
expect(timeSeriesQueryFn(params)).rejects.toThrowErrorMatchingInlineSnapshot(
expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot(
`"invalid date format for dateStart: \\"x\\""`
);
});

View file

@ -5,16 +5,17 @@
*/
import { SearchResponse } from 'elasticsearch';
import { Logger } from 'kibana/server';
import { LegacyScopedClusterClient } from '../../../../../../src/core/server';
import { DEFAULT_GROUPS } from '../index';
import { getDateRangeInfo } from './date_range_info';
import { Logger, CallCluster } from '../../../types';
import { TimeSeriesQuery, TimeSeriesResult, TimeSeriesResultRow } from './time_series_types';
export { TimeSeriesQuery, TimeSeriesResult } from './time_series_types';
export interface TimeSeriesQueryParameters {
logger: Logger;
callCluster: CallCluster;
callCluster: LegacyScopedClusterClient['callAsCurrentUser'];
query: TimeSeriesQuery;
}

View file

@ -10,7 +10,7 @@
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import { parseDuration } from '../../../../../alerts/server';
import { parseDuration } from '../../../../alerts/server';
import { MAX_INTERVALS } from '../index';
import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './core_query_types';
import {
@ -18,11 +18,7 @@ import {
getDateStartAfterDateEndErrorMessage,
} from './date_range_info';
export {
TimeSeriesResult,
TimeSeriesResultRow,
MetricResult,
} from '../../../../common/alert_types/index_threshold';
export { TimeSeriesResult, TimeSeriesResultRow, MetricResult } from '../../../common/data';
// The parameters here are very similar to the alert parameters.
// Missing are `comparator` and `threshold`, which aren't needed to generate
@ -66,9 +62,12 @@ function validateBody(anyParams: unknown): string | undefined {
}
if (epochStart !== epochEnd && !interval) {
return i18n.translate('xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage', {
defaultMessage: '[interval]: must be specified if [dateStart] does not equal [dateEnd]',
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage',
{
defaultMessage: '[interval]: must be specified if [dateStart] does not equal [dateEnd]',
}
);
}
if (interval) {
@ -84,7 +83,7 @@ function validateBody(anyParams: unknown): string | undefined {
function validateDate(dateString: string): string | undefined {
const parsed = Date.parse(dateString);
if (isNaN(parsed)) {
return i18n.translate('xpack.stackAlerts.indexThreshold.invalidDateErrorMessage', {
return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage', {
defaultMessage: 'invalid date {date}',
values: {
date: dateString,
@ -97,11 +96,14 @@ export function validateDuration(duration: string): string | undefined {
try {
parseDuration(duration);
} catch (err) {
return i18n.translate('xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage', {
defaultMessage: 'invalid duration: "{duration}"',
values: {
duration,
},
});
return i18n.translate(
'xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage',
{
defaultMessage: 'invalid duration: "{duration}"',
values: {
duration,
},
}
);
}
}

View file

@ -16,7 +16,7 @@ import {
KibanaResponseFactory,
ILegacyScopedClusterClient,
} from 'kibana/server';
import { Service } from '../../../types';
import { Logger } from '../../../../../../src/core/server';
const bodySchema = schema.object({
indexPatterns: schema.arrayOf(schema.string()),
@ -24,9 +24,9 @@ const bodySchema = schema.object({
type RequestBody = TypeOf<typeof bodySchema>;
export function createFieldsRoute(service: Service, router: IRouter, baseRoute: string) {
export function createFieldsRoute(logger: Logger, router: IRouter, baseRoute: string) {
const path = `${baseRoute}/_fields`;
service.logger.debug(`registering indexThreshold route POST ${path}`);
logger.debug(`registering indexThreshold route POST ${path}`);
router.post(
{
path,
@ -41,7 +41,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute:
req: KibanaRequest<unknown, unknown, RequestBody>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
let rawFields: RawFields;
@ -54,7 +54,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute:
rawFields = await getRawFields(ctx.core.elasticsearch.legacy.client, req.body.indexPatterns);
} catch (err) {
const indexPatterns = req.body.indexPatterns.join(',');
service.logger.warn(
logger.warn(
`route ${path} error getting fields from pattern "${indexPatterns}": ${err.message}`
);
return res.ok({ body: { fields: [] } });
@ -62,7 +62,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute:
const result = { fields: getFieldsFromRawFields(rawFields) };
service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
return res.ok({ body: result });
}
}

View file

@ -4,19 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Service, IRouter } from '../../../types';
import { Logger } from '../../../../../../src/core/server';
import { createTimeSeriesQueryRoute } from './time_series_query';
import { createFieldsRoute } from './fields';
import { createIndicesRoute } from './indices';
import { IRouter } from '../../../../../../src/core/server';
import { getService } from '..';
interface RegisterRoutesParams {
service: Service;
logger: Logger;
router: IRouter;
data: ReturnType<typeof getService>;
baseRoute: string;
}
export function registerRoutes(params: RegisterRoutesParams) {
const { service, router, baseRoute } = params;
createTimeSeriesQueryRoute(service, router, baseRoute);
createFieldsRoute(service, router, baseRoute);
createIndicesRoute(service, router, baseRoute);
const { logger, router, baseRoute, data } = params;
createTimeSeriesQueryRoute(logger, data.timeSeriesQuery, router, baseRoute);
createFieldsRoute(logger, router, baseRoute);
createIndicesRoute(logger, router, baseRoute);
}

View file

@ -19,7 +19,7 @@ import {
ILegacyScopedClusterClient,
} from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { Service } from '../../../types';
import { Logger } from '../../../../../../src/core/server';
const bodySchema = schema.object({
pattern: schema.string(),
@ -27,9 +27,9 @@ const bodySchema = schema.object({
type RequestBody = TypeOf<typeof bodySchema>;
export function createIndicesRoute(service: Service, router: IRouter, baseRoute: string) {
export function createIndicesRoute(logger: Logger, router: IRouter, baseRoute: string) {
const path = `${baseRoute}/_indices`;
service.logger.debug(`registering indexThreshold route POST ${path}`);
logger.debug(`registering indexThreshold route POST ${path}`);
router.post(
{
path,
@ -45,7 +45,7 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute:
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
const pattern = req.body.pattern;
service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
if (pattern.trim() === '') {
return res.ok({ body: { indices: [] } });
@ -55,23 +55,19 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute:
try {
aliases = await getAliasesFromPattern(ctx.core.elasticsearch.legacy.client, pattern);
} catch (err) {
service.logger.warn(
`route ${path} error getting aliases from pattern "${pattern}": ${err.message}`
);
logger.warn(`route ${path} error getting aliases from pattern "${pattern}": ${err.message}`);
}
let indices: string[] = [];
try {
indices = await getIndicesFromPattern(ctx.core.elasticsearch.legacy.client, pattern);
} catch (err) {
service.logger.warn(
`route ${path} error getting indices from pattern "${pattern}": ${err.message}`
);
logger.warn(`route ${path} error getting indices from pattern "${pattern}": ${err.message}`);
}
const result = { indices: uniqueCombined(aliases, indices, MAX_INDICES) };
service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
return res.ok({ body: result });
}
}

View file

@ -11,14 +11,20 @@ import {
IKibanaResponse,
KibanaResponseFactory,
} from 'kibana/server';
import { Logger } from '../../../../../../src/core/server';
import { TimeSeriesQueryParameters } from '../lib/time_series_query';
import { Service } from '../../../types';
import { TimeSeriesQuery, TimeSeriesQuerySchema } from '../lib/time_series_types';
import { TimeSeriesQuery, TimeSeriesQuerySchema, TimeSeriesResult } from '../lib/time_series_types';
export { TimeSeriesQuery, TimeSeriesResult } from '../lib/time_series_types';
export function createTimeSeriesQueryRoute(service: Service, router: IRouter, baseRoute: string) {
export function createTimeSeriesQueryRoute(
logger: Logger,
timeSeriesQuery: (params: TimeSeriesQueryParameters) => Promise<TimeSeriesResult>,
router: IRouter,
baseRoute: string
) {
const path = `${baseRoute}/_time_series_query`;
service.logger.debug(`registering indexThreshold route POST ${path}`);
logger.debug(`registering indexThreshold route POST ${path}`);
router.post(
{
path,
@ -33,15 +39,15 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba
req: KibanaRequest<unknown, unknown, TimeSeriesQuery>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`);
const result = await service.indexThreshold.timeSeriesQuery({
logger: service.logger,
const result = await timeSeriesQuery({
logger,
callCluster: ctx.core.elasticsearch.legacy.client.callAsCurrentUser,
query: req.body,
});
service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
logger.debug(`route ${path} response: ${JSON.stringify(result)}`);
return res.ok({ body: result });
}
}

View file

@ -4,8 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginConfigDescriptor } from 'kibana/server';
import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
import { configSchema, ConfigSchema } from '../config';
import { TriggersActionsPlugin } from './plugin';
export { PluginStartContract } from './plugin';
export {
TimeSeriesQuery,
CoreQueryParams,
CoreQueryParamsSchemaProperties,
validateCoreQueryBody,
MAX_INTERVALS,
MAX_GROUPS,
DEFAULT_GROUPS,
} from './data';
export const config: PluginConfigDescriptor<ConfigSchema> = {
exposeToBrowser: {
@ -14,7 +26,4 @@ export const config: PluginConfigDescriptor<ConfigSchema> = {
schema: configSchema,
};
export const plugin = () => ({
setup() {},
start() {},
});
export const plugin = (ctx: PluginInitializerContext) => new TriggersActionsPlugin(ctx);

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger, Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server';
import { getService, register as registerDataService } from './data';
export interface PluginStartContract {
data: ReturnType<typeof getService>;
}
export class TriggersActionsPlugin implements Plugin<void, PluginStartContract> {
private readonly logger: Logger;
private readonly data: PluginStartContract['data'];
constructor(ctx: PluginInitializerContext) {
this.logger = ctx.logger.get();
this.data = getService();
}
public async setup(core: CoreSetup): Promise<void> {
registerDataService({
logger: this.logger,
data: this.data,
router: core.http.createRouter(),
baseRoute: '/api/triggers_actions_ui',
});
}
public async start(): Promise<PluginStartContract> {
return {
data: this.data,
};
}
public async stop(): Promise<void> {}
}

View file

@ -15,7 +15,6 @@ import {
ObjectRemover,
} from '../../../../../common/lib';
import { createEsDocuments } from './create_test_data';
import { getAlertType } from '../../../../../../../plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/';
const ALERT_TYPE_ID = '.index-threshold';
const ACTION_TYPE_ID = '.index';
@ -27,7 +26,7 @@ const ALERT_INTERVALS_TO_WRITE = 5;
const ALERT_INTERVAL_SECONDS = 3;
const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000;
const DefaultActionMessage = getAlertType().defaultActionMessage;
const DefaultActionMessage = `alert {{alertName}} group {{context.group}} value {{context.value}} exceeded threshold {{context.function}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} on {{context.date}}`;
// eslint-disable-next-line import/no-default-export
export default function alertTests({ getService }: FtrProviderContext) {
@ -65,10 +64,6 @@ export default function alertTests({ getService }: FtrProviderContext) {
await esTestIndexToolOutput.destroy();
});
it('has a default action message', () => {
expect(DefaultActionMessage).to.be.ok();
});
// The tests below create two alerts, one that will fire, one that will
// never fire; the tests ensure the ones that should fire, do fire, and
// those that shouldn't fire, do not fire.

View file

@ -10,7 +10,7 @@ import { Spaces } from '../../../../scenarios';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib';
const API_URI = 'api/stack_alerts/index_threshold/_fields';
const API_URI = 'api/triggers_actions_ui/data/_fields';
// eslint-disable-next-line import/no-default-export
export default function fieldsEndpointTests({ getService }: FtrProviderContext) {

View file

@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib';
import { createEsDocuments } from './create_test_data';
const API_URI = 'api/stack_alerts/index_threshold/_indices';
const API_URI = 'api/triggers_actions_ui/data/_indices';
// eslint-disable-next-line import/no-default-export
export default function indicesEndpointTests({ getService }: FtrProviderContext) {

View file

@ -9,11 +9,11 @@ import expect from '@kbn/expect';
import { Spaces } from '../../../../scenarios';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib';
import { TimeSeriesQuery } from '../../../../../../../plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query';
import { TimeSeriesQuery } from '../../../../../../../plugins/triggers_actions_ui/server';
import { createEsDocuments } from './create_test_data';
const INDEX_THRESHOLD_TIME_SERIES_QUERY_URL = 'api/stack_alerts/index_threshold/_time_series_query';
const INDEX_THRESHOLD_TIME_SERIES_QUERY_URL = 'api/triggers_actions_ui/data/_time_series_query';
const START_DATE_MM_DD_HH_MM_SS_MS = '01-01T00:00:00.000Z';
const START_DATE = `2020-${START_DATE_MM_DD_HH_MM_SS_MS}`;