diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 73e2e9cbf9b76..1708b2f0ae016 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -16,24 +16,28 @@ import { EuiSelect, EuiSpacer, EuiComboBox, - EuiFieldNumber, EuiComboBoxOptionProps, - EuiText, EuiFormRow, EuiCallOut, } from '@elastic/eui'; -import { Alert } from '../../../../types'; -import { Comparator, AggregationType, GroupByType } from './types'; -import { AGGREGATION_TYPES, COMPARATORS } from './constants'; +import { COMPARATORS, builtInComparators } from '../../../../common/constants'; import { getMatchingIndicesForThresholdAlertType, getThresholdAlertTypeFields, loadIndexPatterns, } from './lib/api'; -import { useAppDependencies } from '../../../app_context'; -import { getTimeOptions, getTimeFieldOptions } from '../../../lib/get_time_options'; -import { getTimeUnitLabel } from '../../../lib/get_time_unit_label'; +import { getTimeFieldOptions } from '../../../../common/lib/get_time_options'; import { ThresholdVisualization } from './visualization'; +import { WhenExpression } from '../../../../common'; +import { + OfExpression, + ThresholdExpression, + ForLastExpression, + GroupByExpression, +} from '../../../../common'; +import { builtInAggregationTypes } from '../../../../common/constants'; +import { IndexThresholdAlertParams } from './types'; +import { AlertsContextValue } from '../../../context/alerts_context'; const DEFAULT_VALUES = { AGGREGATION_TYPE: 'count', @@ -58,133 +62,21 @@ const expressionFieldsWithValidation = [ 'timeWindowSize', ]; -export const aggregationTypes: { [key: string]: AggregationType } = { - count: { - text: 'count()', - fieldRequired: false, - value: AGGREGATION_TYPES.COUNT, - validNormalizedTypes: [], - }, - avg: { - text: 'average()', - fieldRequired: true, - validNormalizedTypes: ['number'], - value: AGGREGATION_TYPES.AVERAGE, - }, - sum: { - text: 'sum()', - fieldRequired: true, - validNormalizedTypes: ['number'], - value: AGGREGATION_TYPES.SUM, - }, - min: { - text: 'min()', - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: AGGREGATION_TYPES.MIN, - }, - max: { - text: 'max()', - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: AGGREGATION_TYPES.MAX, - }, -}; - -export const comparators: { [key: string]: Comparator } = { - [COMPARATORS.GREATER_THAN]: { - text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.comparators.isAboveLabel', - { - defaultMessage: 'Is above', - } - ), - value: COMPARATORS.GREATER_THAN, - requiredValues: 1, - }, - [COMPARATORS.GREATER_THAN_OR_EQUALS]: { - text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.comparators.isAboveOrEqualsLabel', - { - defaultMessage: 'Is above or equals', - } - ), - value: COMPARATORS.GREATER_THAN_OR_EQUALS, - requiredValues: 1, - }, - [COMPARATORS.LESS_THAN]: { - text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.comparators.isBelowLabel', - { - defaultMessage: 'Is below', - } - ), - value: COMPARATORS.LESS_THAN, - requiredValues: 1, - }, - [COMPARATORS.LESS_THAN_OR_EQUALS]: { - text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.comparators.isBelowOrEqualsLabel', - { - defaultMessage: 'Is below or equals', - } - ), - value: COMPARATORS.LESS_THAN_OR_EQUALS, - requiredValues: 1, - }, - [COMPARATORS.BETWEEN]: { - text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.comparators.isBetweenLabel', - { - defaultMessage: 'Is between', - } - ), - value: COMPARATORS.BETWEEN, - requiredValues: 2, - }, -}; - -export const groupByTypes: { [key: string]: GroupByType } = { - all: { - text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.groupByLabel.allDocumentsLabel', - { - defaultMessage: 'all documents', - } - ), - sizeRequired: false, - value: 'all', - validNormalizedTypes: [], - }, - top: { - text: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.groupByLabel.topLabel', - { - defaultMessage: 'top', - } - ), - sizeRequired: true, - value: 'top', - validNormalizedTypes: ['number', 'date', 'keyword'], - }, -}; - -interface Props { - alert: Alert; +interface IndexThresholdProps { + alertParams: IndexThresholdAlertParams; setAlertParams: (property: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; errors: { [key: string]: string[] }; - hasErrors?: boolean; + alertsContext: AlertsContextValue; } -export const IndexThresholdAlertTypeExpression: React.FunctionComponent = ({ - alert, +export const IndexThresholdAlertTypeExpression: React.FunctionComponent = ({ + alertParams, setAlertParams, setAlertProperty, errors, + alertsContext, }) => { - const { http } = useAppDependencies(); - const { index, timeField, @@ -197,7 +89,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent = threshold, timeWindowSize, timeWindowUnit, - } = alert.params; + } = alertParams; const firstFieldOption = { text: i18n.translate( @@ -208,31 +100,20 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent = ), value: '', }; + const { http } = alertsContext; - const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); const [indexPopoverOpen, setIndexPopoverOpen] = useState(false); const [indexPatterns, setIndexPatterns] = useState([]); const [esFields, setEsFields] = useState>([]); const [indexOptions, setIndexOptions] = useState([]); const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); - const [alertThresholdPopoverOpen, setAlertThresholdPopoverOpen] = useState(false); - const [alertDurationPopoverOpen, setAlertDurationPopoverOpen] = useState(false); - const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false); - const [groupByPopoverOpen, setGroupByPopoverOpen] = useState(false); - - const andThresholdText = i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.andLabel', - { - defaultMessage: 'AND', - } - ); const hasExpressionErrors = !!Object.keys(errors).find( errorKey => expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1 && - alert.params[errorKey] !== undefined + (alertParams as { [key: string]: any })[errorKey] !== undefined ); const canShowVizualization = !!Object.keys(errors).find( @@ -490,426 +371,77 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent = - { - setAggTypePopoverOpen(true); - }} - /> + + setAlertParams('aggType', selectedAggType) } - isOpen={aggTypePopoverOpen} - closePopover={() => { - setAggTypePopoverOpen(false); - }} - ownFocus - withTitle - anchorPosition="downLeft" - > -
- - {i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.whenButtonLabel', - { - defaultMessage: 'when', - } - )} - - { - setAlertParams('aggType', e.target.value); - setAggTypePopoverOpen(false); - }} - options={Object.values(aggregationTypes).map(({ text, value }) => { - return { - text, - value, - }; - })} - /> -
-
+ />
- {aggType && aggregationTypes[aggType].fieldRequired ? ( + {aggType && builtInAggregationTypes[aggType].fieldRequired ? ( - { - setAggFieldPopoverOpen(true); - }} - color={aggField ? 'secondary' : 'danger'} - /> + + setAlertParams('aggField', selectedAggField) } - isOpen={aggFieldPopoverOpen} - closePopover={() => { - setAggFieldPopoverOpen(false); - }} - anchorPosition="downLeft" - > -
- - {i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.ofButtonLabel', - { - defaultMessage: 'of', - } - )} - - - - 0 && aggField !== undefined} - error={errors.aggField} - > - 0 && aggField !== undefined} - placeholder={firstFieldOption.text} - options={esFields.reduce((esFieldOptions: any[], field: any) => { - if ( - aggregationTypes[aggType].validNormalizedTypes.includes( - field.normalizedType - ) - ) { - esFieldOptions.push({ - label: field.name, - }); - } - return esFieldOptions; - }, [])} - selectedOptions={aggField ? [{ label: aggField }] : []} - onChange={selectedOptions => { - setAlertParams( - 'aggField', - selectedOptions.length === 1 ? selectedOptions[0].label : undefined - ); - setAggFieldPopoverOpen(false); - }} - /> - - - -
-
+ />
) : null} - { - setGroupByPopoverOpen(true); - }} - color={groupBy === 'all' || (termSize && termField) ? 'secondary' : 'danger'} - /> + setAlertParams('groupBy', selectedGroupBy)} + onChangeSelectedTermField={selectedTermField => + setAlertParams('termField', selectedTermField) } - isOpen={groupByPopoverOpen} - closePopover={() => { - setGroupByPopoverOpen(false); - }} - ownFocus - withTitle - anchorPosition="downLeft" - > -
- - {i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.overButtonLabel', - { - defaultMessage: 'over', - } - )} - - - - { - setAlertParams('termSize', null); - setAlertParams('termField', null); - setAlertParams('groupBy', e.target.value); - }} - options={Object.values(groupByTypes).map(({ text, value }) => { - return { - text, - value, - }; - })} - /> - - - {groupByTypes[groupBy || DEFAULT_VALUES.GROUP_BY].sizeRequired ? ( - - - 0} error={errors.termSize}> - 0} - value={termSize || 0} - onChange={e => { - const { value } = e.target; - const termSizeVal = value !== '' ? parseFloat(value) : value; - setAlertParams('termSize', termSizeVal); - }} - min={1} - /> - - - - 0 && termField !== undefined} - error={errors.termField} - > - 0 && termField !== undefined} - onChange={e => { - setAlertParams('termField', e.target.value); - }} - options={esFields.reduce( - (options: any, field: any) => { - if ( - groupByTypes[ - groupBy || DEFAULT_VALUES.GROUP_BY - ].validNormalizedTypes.includes(field.normalizedType) - ) { - options.push({ - text: field.name, - value: field.name, - }); - } - return options; - }, - [firstFieldOption] - )} - /> - - - - ) : null} - -
-
+ onChangeSelectedTermSize={selectedTermSize => + setAlertParams('termSize', selectedTermSize) + } + />
- { - setAlertThresholdPopoverOpen(true); - }} - color={ - (errors.threshold0 && errors.threshold0.length) || - (errors.threshold1 && errors.threshold1.length) - ? 'danger' - : 'secondary' - } - /> + + setAlertParams('threshold', selectedThresholds) } - isOpen={alertThresholdPopoverOpen} - closePopover={() => { - setAlertThresholdPopoverOpen(false); - }} - ownFocus - withTitle - anchorPosition="downLeft" - > -
- - {comparators[thresholdComparator || DEFAULT_VALUES.THRESHOLD_COMPARATOR].text} - - - - { - setAlertParams('thresholdComparator', e.target.value); - }} - options={Object.values(comparators).map(({ text, value }) => { - return { text, value }; - })} - /> - - {Array.from( - Array( - comparators[thresholdComparator || DEFAULT_VALUES.THRESHOLD_COMPARATOR] - .requiredValues - ) - ).map((_notUsed, i) => { - return ( - - {i > 0 ? ( - - {andThresholdText} - {errors[`threshold${i}`].length > 0 && } - - ) : null} - - 0} - error={errors[`threshold${i}`]} - > - 0} - value={!threshold || threshold[i] === null ? 0 : threshold[i]} - min={0} - step={0.1} - onChange={e => { - const { value } = e.target; - const thresholdVal = value !== '' ? parseFloat(value) : value; - const newThreshold = [...threshold]; - newThreshold[i] = thresholdVal; - setAlertParams('threshold', newThreshold); - }} - /> - - - - ); - })} - -
-
+ onChangeSelectedThresholdComparator={selectedThresholdComparator => + setAlertParams('thresholdComparator', selectedThresholdComparator) + } + />
- { - setAlertDurationPopoverOpen(true); - }} - color={timeWindowSize ? 'secondary' : 'danger'} - /> + + setAlertParams('timeWindowSize', selectedWindowSize) } - isOpen={alertDurationPopoverOpen} - closePopover={() => { - setAlertDurationPopoverOpen(false); - }} - ownFocus - withTitle - anchorPosition="downLeft" - > -
- - - - - - 0} - error={errors.timeWindowSize} - > - 0} - min={1} - value={timeWindowSize ? parseInt(timeWindowSize, 10) : 1} - onChange={e => { - const { value } = e.target; - const timeWindowSizeVal = value !== '' ? parseInt(value, 10) : value; - setAlertParams('timeWindowSize', timeWindowSizeVal); - }} - /> - - - - { - setAlertParams('timeWindowUnit', e.target.value); - }} - options={getTimeOptions(timeWindowSize)} - /> - - -
-
+ onChangeWindowUnit={(selectedWindowUnit: any) => + setAlertParams('timeWindowUnit', selectedWindowUnit) + } + />
{canShowVizualization ? null : ( - + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts index a0a5b05d49680..e388149311075 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { Alert, AlertTypeModel, ValidationResult } from '../../../../types'; -import { IndexThresholdAlertTypeExpression, aggregationTypes, groupByTypes } from './expression'; +import { AlertTypeModel, ValidationResult } from '../../../../types'; +import { IndexThresholdAlertTypeExpression } from './expression'; +import { IndexThresholdAlertParams } from './types'; +import { builtInGroupByTypes, builtInAggregationTypes } from '../../../../common/constants'; export function getAlertType(): AlertTypeModel { return { @@ -13,7 +15,7 @@ export function getAlertType(): AlertTypeModel { name: 'Index Threshold', iconClass: 'alert', alertParamsExpression: IndexThresholdAlertTypeExpression, - validate: (alert: Alert): ValidationResult => { + validate: (alertParams: IndexThresholdAlertParams): ValidationResult => { const { index, timeField, @@ -24,7 +26,7 @@ export function getAlertType(): AlertTypeModel { termField, threshold, timeWindowSize, - } = alert.params; + } = alertParams; const validationResult = { errors: {} }; const errors = { aggField: new Array(), @@ -51,7 +53,7 @@ export function getAlertType(): AlertTypeModel { }) ); } - if (aggType && aggregationTypes[aggType].fieldRequired && !aggField) { + if (aggType && builtInAggregationTypes[aggType].fieldRequired && !aggField) { errors.aggField.push( i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText', { defaultMessage: 'Aggregation field is required.', @@ -65,7 +67,7 @@ export function getAlertType(): AlertTypeModel { }) ); } - if (groupBy && groupByTypes[groupBy].sizeRequired && !termField) { + if (!termField && groupBy && builtInGroupByTypes[groupBy].sizeRequired) { errors.termField.push( i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText', { defaultMessage: 'Term field is required.', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js index 000ad752da76e..f49e85ddefea8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js @@ -24,9 +24,9 @@ function isValidMoment(m) { * @param {state} object - one of "" * @param {[type]} display [description] */ -function TimeBuckets(uiSettings, dataPlugin) { +function TimeBuckets(uiSettings, dataFieldsFormats) { this.uiSettings = uiSettings; - this.dataPlugin = dataPlugin; + this.dataFieldsFormats = dataFieldsFormats; return TimeBuckets.__cached__(this); } @@ -220,14 +220,14 @@ TimeBuckets.prototype.getInterval = function(useNormalizedEsInterval = true) { function readInterval() { const interval = self._i; if (moment.isDuration(interval)) return interval; - return calcAutoIntervalNear(this.uiSettings.get('histogram:barTarget'), Number(duration)); + return calcAutoIntervalNear(self.uiSettings.get('histogram:barTarget'), Number(duration)); } // check to see if the interval should be scaled, and scale it if so function maybeScaleInterval(interval) { if (!self.hasBounds()) return interval; - const maxLength = this.uiSettings.get('histogram:maxBars'); + const maxLength = self.uiSettings.get('histogram:maxBars'); const approxLen = duration / interval; let scaled; @@ -294,7 +294,7 @@ TimeBuckets.prototype.getScaledDateFormat = function() { }; TimeBuckets.prototype.getScaledDateFormatter = function() { - const fieldFormatsService = this.dataPlugin.fieldFormats; + const fieldFormatsService = this.dataFieldsFormats; const DateFieldFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE); return new DateFieldFormat( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts index fd2a401fe59f3..356b0fbbc0845 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts @@ -23,3 +23,17 @@ export interface GroupByType { value: string; validNormalizedTypes: string[]; } + +export interface IndexThresholdAlertParams { + index: string[]; + timeField?: string; + aggType: string; + aggField?: string; + groupBy?: string; + termSize?: number; + termField?: string; + thresholdComparator?: string; + threshold: number[]; + timeWindowSize: number; + timeWindowUnit: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index bc5854bcc2ca8..ce7ef0dd39bac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -23,13 +23,12 @@ import dateMath from '@elastic/datemath'; import moment from 'moment-timezone'; import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getThresholdAlertVisualizationData } from './lib/api'; +import { AggregationType, Comparator } from '../../../../common/types'; /* TODO: This file was copied from ui/time_buckets for NP migration. We should clean this up and add TS support */ import { TimeBuckets } from './lib/time_buckets'; -import { getThresholdAlertVisualizationData } from './lib/api'; -import { comparators, aggregationTypes } from './expression'; -import { useAppDependencies } from '../../../app_context'; -import { Alert } from '../../../../types'; -import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; +import { AlertsContextValue } from '../../../context/alerts_context'; +import { IndexThresholdAlertParams } from './types'; const customTheme = () => { return { @@ -77,40 +76,35 @@ const getDomain = (alertParams: any) => { }; }; -const getThreshold = (alertParams: any) => { - return alertParams.threshold.slice( - 0, - comparators[alertParams.thresholdComparator].requiredValues - ); -}; - const getTimeBuckets = ( uiSettings: IUiSettingsClient, - dataPlugin: DataPublicPluginStart, + dataFieldsFormats: any, alertParams: any ) => { const domain = getDomain(alertParams); - const timeBuckets = new TimeBuckets(uiSettings, dataPlugin); + const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats); timeBuckets.setBounds(domain); return timeBuckets; }; interface Props { - alert: Alert; + alertParams: IndexThresholdAlertParams; + aggregationTypes: { [key: string]: AggregationType }; + comparators: { + [key: string]: Comparator; + }; + alertsContext: AlertsContextValue; } -export const ThresholdVisualization: React.FunctionComponent = ({ alert }) => { - const { http, uiSettings, toastNotifications, charts, dataPlugin } = useAppDependencies(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - const [visualizationData, setVisualizationData] = useState>([]); - - const chartsTheme = charts.theme.useChartsTheme(); +export const ThresholdVisualization: React.FunctionComponent = ({ + alertParams, + aggregationTypes, + comparators, + alertsContext, +}) => { const { index, timeField, - triggerIntervalSize, - triggerIntervalUnit, aggType, aggField, termSize, @@ -120,21 +114,12 @@ export const ThresholdVisualization: React.FunctionComponent = ({ alert } timeWindowUnit, groupBy, threshold, - } = alert.params; + } = alertParams; + const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext; - const domain = getDomain(alert.params); - const timeBuckets = new TimeBuckets(uiSettings, dataPlugin); - timeBuckets.setBounds(domain); - const interval = timeBuckets.getInterval().expression; - const visualizeOptions = { - rangeFrom: domain.min, - rangeTo: domain.max, - interval, - timezone: getTimezone(uiSettings), - }; - - // Fetching visualization data is independent of alert actions - const alertWithoutActions = { ...alert.params, actions: [], type: 'threshold' }; + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const [visualizationData, setVisualizationData] = useState>([]); useEffect(() => { (async () => { @@ -148,12 +133,14 @@ export const ThresholdVisualization: React.FunctionComponent = ({ alert } }) ); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage', - { defaultMessage: 'Unable to load visualization' } - ), - }); + if (toastNotifications) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage', + { defaultMessage: 'Unable to load visualization' } + ), + }); + } setError(e); } finally { setIsLoading(false); @@ -163,8 +150,6 @@ export const ThresholdVisualization: React.FunctionComponent = ({ alert } }, [ index, timeField, - triggerIntervalSize, - triggerIntervalUnit, aggType, aggField, termSize, @@ -177,6 +162,25 @@ export const ThresholdVisualization: React.FunctionComponent = ({ alert } ]); /* eslint-enable react-hooks/exhaustive-deps */ + if (!charts || !uiSettings || !dataFieldsFormats) { + return null; + } + const chartsTheme = charts.theme.useChartsTheme(); + + const domain = getDomain(alertParams); + const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats); + timeBuckets.setBounds(domain); + const interval = timeBuckets.getInterval().expression; + const visualizeOptions = { + rangeFrom: domain.min, + rangeTo: domain.max, + interval, + timezone: getTimezone(uiSettings), + }; + + // Fetching visualization data is independent of alert actions + const alertWithoutActions = { ...alertParams, actions: [], type: 'threshold' }; + if (isLoading) { return ( = ({ alert } ); } + const getThreshold = () => { + return thresholdComparator + ? threshold.slice(0, comparators[thresholdComparator].requiredValues) + : []; + }; + if (visualizationData) { const alertVisualizationDataKeys = Object.keys(visualizationData); const timezone = getTimezone(uiSettings); - const actualThreshold = getThreshold(alert.params); - let maxY = actualThreshold[actualThreshold.length - 1]; + const actualThreshold = getThreshold(); + let maxY = actualThreshold[actualThreshold.length - 1] as any; (Object.values(visualizationData) as number[][][]).forEach(data => { data.forEach(([, y]) => { @@ -231,7 +241,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ alert } const dateFormatter = (d: number) => { return moment(d) .tz(timezone) - .format(getTimeBuckets(uiSettings, dataPlugin, alert.params).getScaledDateFormat()); + .format(getTimeBuckets(uiSettings, dataFieldsFormats, alertParams).getScaledDateFormat()); }; const aggLabel = aggregationTypes[aggType].text; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx index e019319d843a8..9b6b4a2cf1f22 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx @@ -5,11 +5,26 @@ */ import React, { useContext, createContext } from 'react'; +import { HttpSetup, IUiSettingsClient, ToastsApi } from 'kibana/public'; +import { ChartsPluginSetup } from 'src/plugins/charts/public'; +import { FieldFormatsRegistry } from 'src/plugins/data/common/field_formats/static'; +import { TypeRegistry } from '../type_registry'; +import { AlertTypeModel, ActionTypeModel } from '../../types'; export interface AlertsContextValue { addFlyoutVisible: boolean; setAddFlyoutVisibility: React.Dispatch>; - reloadAlerts: () => Promise; + reloadAlerts?: () => Promise; + http: HttpSetup; + alertTypeRegistry: TypeRegistry; + actionTypeRegistry: TypeRegistry; + uiSettings?: IUiSettingsClient; + toastNotifications?: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + charts?: ChartsPluginSetup; + dataFieldsFormats?: Pick; } const AlertsContext = createContext(null as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index b530e803bf285..f7becb16c244a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -9,32 +9,26 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, ActionConnector } from '../../../types'; import { ActionConnectorForm } from './action_connector_form'; -import { AppContextProvider } from '../../app_context'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; - const actionTypeRegistry = actionTypeRegistryMock.create(); describe('action_connector_form', () => { let deps: any; beforeAll(async () => { - const mockes = coreMock.createSetup(); + const mocks = coreMock.createSetup(); const [ { chrome, docLinks, application: { capabilities }, }, - ] = await mockes.getStartServices(); + ] = await mocks.getStartServices(); deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, - http: mockes.http, - uiSettings: mockes.uiSettings, + toastNotifications: mocks.notifications.toasts, + injectedMetadata: mocks.injectedMetadata, + http: mocks.http, + uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { @@ -43,7 +37,9 @@ describe('action_connector_form', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), + legacy: { + MANAGEMENT_BREADCRUMB: { set: () => {} } as any, + }, actionTypeRegistry: actionTypeRegistry as any, alertTypeRegistry: {} as any, }; @@ -72,19 +68,21 @@ describe('action_connector_form', () => { config: {}, secrets: {}, } as ActionConnector; - const wrapper = mountWithIntl( - + let wrapper; + if (deps) { + wrapper = mountWithIntl( {}} serverError={null} errors={{ name: [] }} + actionTypeRegistry={deps.actionTypeRegistry} /> - - ); - const connectorNameField = wrapper.find('[data-test-subj="nameInput"]'); - expect(connectorNameField.exists()).toBeTruthy(); - expect(connectorNameField.first().prop('value')).toBe(''); + ); + } + const connectorNameField = wrapper?.find('[data-test-subj="nameInput"]'); + expect(connectorNameField?.exists()).toBeTruthy(); + expect(connectorNameField?.first().prop('value')).toBe(''); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index c29064efcde35..9f2b203246280 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -15,9 +15,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useAppDependencies } from '../../app_context'; import { ReducerAction } from './connector_reducer'; -import { ActionConnector, IErrorObject } from '../../../types'; +import { ActionConnector, IErrorObject, ActionTypeModel } from '../../../types'; +import { TypeRegistry } from '../../type_registry'; export function validateBaseProperties(actionObject: ActionConnector) { const validationResult = { errors: {} }; @@ -46,6 +46,7 @@ interface ActionConnectorProps { body: { message: string; error: string }; } | null; errors: IErrorObject; + actionTypeRegistry: TypeRegistry; } export const ActionConnectorForm = ({ @@ -54,9 +55,8 @@ export const ActionConnectorForm = ({ actionTypeName, serverError, errors, + actionTypeRegistry, }: ActionConnectorProps) => { - const { actionTypeRegistry } = useAppDependencies(); - const setActionProperty = (key: string, value: any) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index 7d90198ad641f..c1c6d9d94e810 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -10,10 +10,6 @@ import { ActionsConnectorsContextProvider } from '../../context/actions_connecto import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ActionTypeMenu } from './action_type_menu'; import { ValidationResult } from '../../../types'; -import { AppContextProvider } from '../../app_context'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; - const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_flyout', () => { @@ -31,8 +27,6 @@ describe('connector_add_flyout', () => { deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, injectedMetadata: mockes.injectedMetadata, http: mockes.http, @@ -45,7 +39,9 @@ describe('connector_add_flyout', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), + legacy: { + MANAGEMENT_BREADCRUMB: { set: () => {} } as any, + }, actionTypeRegistry: actionTypeRegistry as any, alertTypeRegistry: {} as any, }; @@ -70,25 +66,26 @@ describe('connector_add_flyout', () => { actionTypeRegistry.get.mockReturnValueOnce(actionType); const wrapper = mountWithIntl( - - {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, - 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, - }, - reloadConnectors: () => { - return new Promise(() => {}); - }, - }} - > - - - + {}, + editFlyoutVisible: false, + setEditFlyoutVisibility: state => {}, + actionTypesIndex: { + 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, + 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, + }, + reloadConnectors: () => { + return new Promise(() => {}); + }, + }} + > + + ); expect(wrapper.find('[data-test-subj="first-action-type-card"]').exists()).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index f89d61acb59ca..ddd08cf6d6d79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -5,16 +5,16 @@ */ import React from 'react'; import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid } from '@elastic/eui'; -import { ActionType } from '../../../types'; +import { ActionType, ActionTypeModel } from '../../../types'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; -import { useAppDependencies } from '../../app_context'; +import { TypeRegistry } from '../../type_registry'; interface Props { onActionTypeChange: (actionType: ActionType) => void; + actionTypeRegistry: TypeRegistry; } -export const ActionTypeMenu = ({ onActionTypeChange }: Props) => { - const { actionTypeRegistry } = useAppDependencies(); +export const ActionTypeMenu = ({ onActionTypeChange, actionTypeRegistry }: Props) => { const { actionTypesIndex } = useActionsConnectorsContext(); if (!actionTypesIndex) { return null; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index e2faca5661d1e..6b87002a1d2cf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -21,23 +21,23 @@ describe('connector_add_flyout', () => { let deps: AppDeps | null; beforeAll(async () => { - const mockes = coreMock.createSetup(); + const mocks = coreMock.createSetup(); const [ { chrome, docLinks, application: { capabilities }, }, - ] = await mockes.getStartServices(); + ] = await mocks.getStartServices(); deps = { chrome, docLinks, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), - toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, - http: mockes.http, - uiSettings: mockes.uiSettings, + toastNotifications: mocks.notifications.toasts, + injectedMetadata: mocks.injectedMetadata, + http: mocks.http, + uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 8a50513f158a1..0749ae1d30e9e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -77,7 +77,12 @@ export const ConnectorAddFlyout = () => { let currentForm; let actionTypeModel; if (!actionType) { - currentForm = ; + currentForm = ( + + ); } else { actionTypeModel = actionTypeRegistry.get(actionType.id); @@ -94,6 +99,7 @@ export const ConnectorAddFlyout = () => { dispatch={dispatch} serverError={serverError} errors={errors} + actionTypeRegistry={actionTypeRegistry} /> ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index dd10075c33792..d9f3e98919d76 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -10,10 +10,9 @@ import { ConnectorAddModal } from './connector_add_modal'; import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { AppContextProvider } from '../../app_context'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { AppDeps } from '../../app'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_modal', () => { @@ -31,12 +30,12 @@ describe('connector_add_modal', () => { deps = { chrome, docLinks, + dataPlugin: dataPluginMock.createStartContract(), + charts: chartPluginMock.createStartContract(), toastNotifications: mocks.notifications.toasts, injectedMetadata: mocks.injectedMetadata, http: mocks.http, uiSettings: mocks.uiSettings, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), capabilities: { ...capabilities, actions: { @@ -74,31 +73,35 @@ describe('connector_add_modal', () => { enabled: true, }; - const wrapper = mountWithIntl( - - {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, - }, - reloadConnectors: () => { - return new Promise(() => {}); - }, - }} - > - {}} - actionType={actionType} - /> - - - ); - expect(wrapper.find('EuiModalHeader')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="saveActionButtonModal"]').exists()).toBeTruthy(); + const wrapper = deps + ? mountWithIntl( + {}, + editFlyoutVisible: false, + setEditFlyoutVisibility: state => {}, + actionTypesIndex: { + 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, + }, + reloadConnectors: () => { + return new Promise(() => {}); + }, + }} + > + {}} + actionType={actionType} + http={deps.http} + actionTypeRegistry={deps.actionTypeRegistry} + alertTypeRegistry={deps.alertTypeRegistry} + toastNotifications={deps.toastNotifications} + /> + + ) + : undefined; + expect(wrapper?.find('EuiModalHeader')).toHaveLength(1); + expect(wrapper?.find('[data-test-subj="saveActionButtonModal"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 2ce282e946a38..55386ec6d61f9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -17,25 +17,43 @@ import { import { EuiButtonEmpty } from '@elastic/eui'; import { EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { HttpSetup, ToastsApi } from 'kibana/public'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; -import { ActionType, ActionConnector, IErrorObject } from '../../../types'; +import { + ActionType, + ActionConnector, + IErrorObject, + AlertTypeModel, + ActionTypeModel, +} from '../../../types'; import { connectorReducer } from './connector_reducer'; import { createActionConnector } from '../../lib/action_connector_api'; -import { useAppDependencies } from '../../app_context'; +import { TypeRegistry } from '../../type_registry'; + +interface ConnectorAddModalProps { + actionType: ActionType; + addModalVisible: boolean; + setAddModalVisibility: React.Dispatch>; + postSaveEventHandler?: (savedAction: ActionConnector) => void; + http: HttpSetup; + alertTypeRegistry: TypeRegistry; + actionTypeRegistry: TypeRegistry; + toastNotifications?: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; +} export const ConnectorAddModal = ({ actionType, addModalVisible, setAddModalVisibility, postSaveEventHandler, -}: { - actionType: ActionType; - addModalVisible: boolean; - setAddModalVisibility: React.Dispatch>; - postSaveEventHandler?: (savedAction: ActionConnector) => void; -}) => { + http, + toastNotifications, + actionTypeRegistry, +}: ConnectorAddModalProps) => { let hasErrors = false; - const { http, toastNotifications, actionTypeRegistry } = useAppDependencies(); const initialConnector = { actionTypeId: actionType.id, config: {}, @@ -70,17 +88,19 @@ export const ConnectorAddModal = ({ const onActionConnectorSave = async (): Promise => await createActionConnector({ http, connector }) .then(savedConnector => { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Created '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); + if (toastNotifications) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText', + { + defaultMessage: "Created '{connectorName}'", + values: { + connectorName: savedConnector.name, + }, + } + ) + ); + } return savedConnector; }) .catch(errorRes => { @@ -123,6 +143,7 @@ export const ConnectorAddModal = ({ dispatch={dispatch} serverError={serverError} errors={errors} + actionTypeRegistry={actionTypeRegistry} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 9c1f2ddc7f7f6..f7ad6f95d048f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -108,6 +108,7 @@ export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => errors={errors} actionTypeName={connector.actionType} dispatch={dispatch} + actionTypeRegistry={actionTypeRegistry} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx index efe85010a3a7e..05adccf982b7f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx @@ -10,7 +10,6 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { AlertAdd } from './alert_add'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { AppContextProvider } from '../../app_context'; import { AppDeps } from '../../app'; import { AlertsContextProvider } from '../../context/alerts_context'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; @@ -87,8 +86,8 @@ describe('alert_add', () => { actionTypeRegistry.has.mockReturnValue(true); await act(async () => { - wrapper = mountWithIntl( - + if (deps) { + wrapper = mountWithIntl( { reloadAlerts: () => { return new Promise(() => {}); }, + http: deps.http, + actionTypeRegistry: deps.actionTypeRegistry, + alertTypeRegistry: deps.alertTypeRegistry, + toastNotifications: deps.toastNotifications, + uiSettings: deps.uiSettings, }} > - + - - ); + ); + } }); await waitForRender(wrapper); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx index d73feff938076..a88f916346985 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.tsx @@ -22,15 +22,19 @@ import { useAlertsContext } from '../../context/alerts_context'; import { Alert, AlertAction, IErrorObject } from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; -import { useAppDependencies } from '../../app_context'; import { createAlert } from '../../lib/alert_api'; -export const AlertAdd = () => { - const { http, toastNotifications, alertTypeRegistry, actionTypeRegistry } = useAppDependencies(); +interface AlertAddProps { + consumer: string; + alertTypeId?: string; + canChangeTrigger?: boolean; +} + +export const AlertAdd = ({ consumer, canChangeTrigger, alertTypeId }: AlertAddProps) => { const initialAlert = ({ params: {}, - consumer: 'alerting', - alertTypeId: null, + consumer, + alertTypeId, schedule: { interval: '1m', }, @@ -45,7 +49,15 @@ export const AlertAdd = () => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; - const { addFlyoutVisible, setAddFlyoutVisibility, reloadAlerts } = useAlertsContext(); + const { + addFlyoutVisible, + setAddFlyoutVisibility, + reloadAlerts, + http, + toastNotifications, + alertTypeRegistry, + actionTypeRegistry, + } = useAlertsContext(); const closeFlyout = useCallback(() => { setAddFlyoutVisibility(false); @@ -63,7 +75,7 @@ export const AlertAdd = () => { const alertType = alertTypeRegistry.get(alert.alertTypeId); const errors = { - ...(alertType ? alertType.validate(alert).errors : []), + ...(alertType ? alertType.validate(alert.params).errors : []), ...validateBaseProperties(alert).errors, } as IErrorObject; const hasErrors = !!Object.keys(errors).find(errorKey => errors[errorKey].length >= 1); @@ -91,14 +103,16 @@ export const AlertAdd = () => { async function onSaveAlert(): Promise { try { const newAlert = await createAlert({ http, alert }); - toastNotifications.addSuccess( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.saveSuccessNotificationText', { - defaultMessage: "Saved '{alertName}'", - values: { - alertName: newAlert.name, - }, - }) - ); + if (toastNotifications) { + toastNotifications.addSuccess( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.saveSuccessNotificationText', { + defaultMessage: "Saved '{alertName}'", + values: { + alertName: newAlert.name, + }, + }) + ); + } return newAlert; } catch (errorRes) { setServerError(errorRes); @@ -126,7 +140,13 @@ export const AlertAdd = () => { - + @@ -152,7 +172,9 @@ export const AlertAdd = () => { setIsSaving(false); if (savedAlert) { closeFlyout(); - reloadAlerts(); + if (reloadAlerts) { + reloadAlerts(); + } } }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx index b5be1c852726f..ce524ed8178ee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx @@ -12,10 +12,10 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { ValidationResult, Alert } from '../../../types'; import { AlertForm } from './alert_form'; -import { AppContextProvider } from '../../app_context'; import { AppDeps } from '../../app'; import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { AlertsContextProvider } from '../../context/alerts_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); describe('alert_form', () => { @@ -100,16 +100,31 @@ describe('alert_form', () => { } as unknown) as Alert; await act(async () => { - wrapper = mountWithIntl( - - {}} - errors={{ name: [] }} - serverError={null} - /> - - ); + if (deps) { + wrapper = mountWithIntl( + {}, + reloadAlerts: () => { + return new Promise(() => {}); + }, + http: deps.http, + actionTypeRegistry: deps.actionTypeRegistry, + alertTypeRegistry: deps.alertTypeRegistry, + toastNotifications: deps.toastNotifications, + uiSettings: deps.uiSettings, + }} + > + {}} + errors={{ name: [] }} + serverError={null} + /> + + ); + } }); await waitForRender(wrapper); @@ -160,16 +175,31 @@ describe('alert_form', () => { } as unknown) as Alert; await act(async () => { - wrapper = mountWithIntl( - - {}} - errors={{ name: [] }} - serverError={null} - /> - - ); + if (deps) { + wrapper = mountWithIntl( + {}, + reloadAlerts: () => { + return new Promise(() => {}); + }, + http: deps.http, + actionTypeRegistry: deps.actionTypeRegistry, + alertTypeRegistry: deps.alertTypeRegistry, + toastNotifications: deps.toastNotifications, + uiSettings: deps.uiSettings, + }} + > + {}} + errors={{ name: [] }} + serverError={null} + /> + + ); + } }); await waitForRender(wrapper); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx index 78aca3ec78e66..90b84e11fccd2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx @@ -28,7 +28,6 @@ import { EuiEmptyPrompt, EuiButtonEmpty, } from '@elastic/eui'; -import { useAppDependencies } from '../../app_context'; import { loadAlertTypes } from '../../lib/alert_api'; import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; import { AlertReducerAction } from './alert_reducer'; @@ -42,9 +41,10 @@ import { ActionConnector, AlertTypeIndex, } from '../../../types'; -import { getTimeOptions } from '../../lib/get_time_options'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from '../action_connector_form/connector_add_modal'; +import { getTimeOptions } from '../../../common/lib/get_time_options'; +import { useAlertsContext } from '../../context/alerts_context'; export function validateBaseProperties(alertObject: Alert) { const validationResult = { errors: {} }; @@ -81,12 +81,12 @@ export function validateBaseProperties(alertObject: Alert) { interface AlertFormProps { alert: Alert; - canChangeTrigger?: boolean; // to hide Change trigger button dispatch: React.Dispatch; errors: IErrorObject; serverError: { body: { message: string; error: string }; } | null; + canChangeTrigger?: boolean; // to hide Change trigger button } interface ActiveActionConnectorState { @@ -101,7 +101,9 @@ export const AlertForm = ({ errors, serverError, }: AlertFormProps) => { - const { http, toastNotifications, alertTypeRegistry, actionTypeRegistry } = useAppDependencies(); + const alertsContext = useAlertsContext(); + const { http, toastNotifications, alertTypeRegistry, actionTypeRegistry } = alertsContext; + const [alertTypeModel, setAlertTypeModel] = useState( alertTypeRegistry.get(alert.alertTypeId) ); @@ -133,12 +135,14 @@ export const AlertForm = ({ } setActionTypesIndex(index); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', - { defaultMessage: 'Unable to load action types' } - ), - }); + if (toastNotifications) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', + { defaultMessage: 'Unable to load action types' } + ), + }); + } } finally { setIsLoadingActionTypes(false); } @@ -162,14 +166,19 @@ export const AlertForm = ({ for (const alertTypeItem of alertTypes) { index[alertTypeItem.id] = alertTypeItem; } + if (alert.alertTypeId) { + setDefaultActionGroup(index[alert.alertTypeId].actionGroups[0]); + } setAlertTypesIndex(index); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage', - { defaultMessage: 'Unable to load alert types' } - ), - }); + if (toastNotifications) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage', + { defaultMessage: 'Unable to load alert types' } + ), + }); + } } })(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -207,14 +216,16 @@ export const AlertForm = ({ const actionsResponse = await loadAllActions({ http }); setConnectors(actionsResponse.data); } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', - { - defaultMessage: 'Unable to load connectors', - } - ), - }); + if (toastNotifications) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', + { + defaultMessage: 'Unable to load connectors', + } + ), + }); + } } } @@ -609,10 +620,11 @@ export const AlertForm = ({ {AlertParamsExpressionComponent ? ( ) : null} @@ -774,7 +786,7 @@ export const AlertForm = ({ fullWidth compressed value={alertIntervalUnit} - options={getTimeOptions((alertInterval ? alertInterval : 1).toString())} + options={getTimeOptions(alertInterval ?? 1)} onChange={e => { setAlertIntervalUnit(e.target.value); setScheduleProperty('interval', `${alertInterval}${e.target.value}`); @@ -806,7 +818,7 @@ export const AlertForm = ({ { setAlertThrottleUnit(e.target.value); setAlertProperty('throttle', `${alertThrottle}${e.target.value}`); @@ -846,6 +858,10 @@ export const AlertForm = ({ connectors.push(savedAction); setActionProperty('id', savedAction.id, activeActionItem.index); }} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + http={http} + toastNotifications={toastNotifications} /> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 643816e728d1a..a89215e6c2964 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -49,7 +49,17 @@ interface AlertState { export const AlertsList: React.FunctionComponent = () => { const history = useHistory(); - const { http, injectedMetadata, toastNotifications, capabilities } = useAppDependencies(); + const { + http, + injectedMetadata, + toastNotifications, + capabilities, + alertTypeRegistry, + actionTypeRegistry, + uiSettings, + charts, + dataPlugin, + } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); const createAlertUiEnabled = injectedMetadata.getInjectedVar('createAlertUiEnabled'); @@ -385,9 +395,16 @@ export const AlertsList: React.FunctionComponent = () => { addFlyoutVisible: alertFlyoutVisible, setAddFlyoutVisibility: setAlertFlyoutVisibility, reloadAlerts: loadAlertsData, + http, + actionTypeRegistry, + alertTypeRegistry, + toastNotifications, + uiSettings, + charts, + dataFieldsFormats: dataPlugin.fieldFormats, }} > - + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts new file mode 100644 index 0000000000000..fb0fde1b9d9a5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts @@ -0,0 +1,48 @@ +/* + * 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 { AggregationType } from '../types'; + +export enum AGGREGATION_TYPES { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', +} + +export const builtInAggregationTypes: { [key: string]: AggregationType } = { + count: { + text: 'count()', + fieldRequired: false, + value: AGGREGATION_TYPES.COUNT, + validNormalizedTypes: [], + }, + avg: { + text: 'average()', + fieldRequired: true, + validNormalizedTypes: ['number'], + value: AGGREGATION_TYPES.AVERAGE, + }, + sum: { + text: 'sum()', + fieldRequired: true, + validNormalizedTypes: ['number'], + value: AGGREGATION_TYPES.SUM, + }, + min: { + text: 'min()', + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MIN, + }, + max: { + text: 'max()', + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MAX, + }, +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts new file mode 100644 index 0000000000000..50c9dd0019e8b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts @@ -0,0 +1,60 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Comparator } from '../types'; + +export enum COMPARATORS { + GREATER_THAN = '>', + GREATER_THAN_OR_EQUALS = '>=', + BETWEEN = 'between', + LESS_THAN = '<', + LESS_THAN_OR_EQUALS = '<=', +} + +export const builtInComparators: { [key: string]: Comparator } = { + [COMPARATORS.GREATER_THAN]: { + text: i18n.translate('xpack.triggersActionsUI.common.constants.comparators.isAboveLabel', { + defaultMessage: 'Is above', + }), + value: COMPARATORS.GREATER_THAN, + requiredValues: 1, + }, + [COMPARATORS.GREATER_THAN_OR_EQUALS]: { + text: i18n.translate( + 'xpack.triggersActionsUI.common.constants.comparators.isAboveOrEqualsLabel', + { + defaultMessage: 'Is above or equals', + } + ), + value: COMPARATORS.GREATER_THAN_OR_EQUALS, + requiredValues: 1, + }, + [COMPARATORS.LESS_THAN]: { + text: i18n.translate('xpack.triggersActionsUI.common.constants.comparators.isBelowLabel', { + defaultMessage: 'Is below', + }), + value: COMPARATORS.LESS_THAN, + requiredValues: 1, + }, + [COMPARATORS.LESS_THAN_OR_EQUALS]: { + text: i18n.translate( + 'xpack.triggersActionsUI.common.constants.comparators.isBelowOrEqualsLabel', + { + defaultMessage: 'Is below or equals', + } + ), + value: COMPARATORS.LESS_THAN_OR_EQUALS, + requiredValues: 1, + }, + [COMPARATORS.BETWEEN]: { + text: i18n.translate('xpack.triggersActionsUI.common.constants.comparators.isBetweenLabel', { + defaultMessage: 'Is between', + }), + value: COMPARATORS.BETWEEN, + requiredValues: 2, + }, +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/group_by_types.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/group_by_types.ts new file mode 100644 index 0000000000000..41964de0fd942 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/group_by_types.ts @@ -0,0 +1,32 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { GroupByType } from '../types'; + +export const builtInGroupByTypes: { [key: string]: GroupByType } = { + all: { + text: i18n.translate( + 'xpack.triggersActionsUI.common.constants.comparators.groupByTypes.allDocumentsLabel', + { + defaultMessage: 'all documents', + } + ), + sizeRequired: false, + value: 'all', + validNormalizedTypes: [], + }, + top: { + text: i18n.translate( + 'xpack.triggersActionsUI.common.constants.comparators.groupByTypes.topLabel', + { + defaultMessage: 'top', + } + ), + sizeRequired: true, + value: 'top', + validNormalizedTypes: ['number', 'date', 'keyword'], + }, +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts new file mode 100644 index 0000000000000..816dc894ab9ec --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { COMPARATORS, builtInComparators } from './comparators'; +export { AGGREGATION_TYPES, builtInAggregationTypes } from './aggregation_types'; +export { builtInGroupByTypes } from './group_by_types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx new file mode 100644 index 0000000000000..6ae3056001c8f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx @@ -0,0 +1,52 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { EuiPopoverTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ForLastExpression } from './for_the_last'; + +describe('for the last expression', () => { + it('renders with defined options', () => { + const onChangeWindowSize = jest.fn(); + const onChangeWindowUnit = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="timeWindowSizeNumber"]').length > 0).toBeTruthy(); + }); + + it('renders with default timeWindowSize and timeWindowUnit', () => { + const onChangeWindowSize = jest.fn(); + const onChangeWindowUnit = jest.fn(); + const wrapper = shallow( + + ); + wrapper.simulate('click'); + expect(wrapper.find('[value=1]').length > 0).toBeTruthy(); + expect(wrapper.find('[value="s"]').length > 0).toBeTruthy(); + expect( + wrapper.contains( + + + + ) + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx new file mode 100644 index 0000000000000..844551de3171d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx @@ -0,0 +1,124 @@ +/* + * 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 React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiExpression, + EuiPopover, + EuiPopoverTitle, + EuiSelect, + EuiFlexGroup, + EuiFormRow, + EuiFlexItem, + EuiFieldNumber, +} from '@elastic/eui'; +import { getTimeUnitLabel } from '../lib/get_time_unit_label'; +import { TIME_UNITS } from '../../application/constants'; +import { getTimeOptions } from '../lib/get_time_options'; + +interface ForLastExpressionProps { + timeWindowSize?: number; + timeWindowUnit?: string; + errors: { [key: string]: string[] }; + onChangeWindowSize: (selectedWindowSize: number | '') => void; + onChangeWindowUnit: (selectedWindowUnit: string) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const ForLastExpression = ({ + timeWindowSize = 1, + timeWindowUnit = 's', + errors, + onChangeWindowSize, + onChangeWindowUnit, + popupPosition, +}: ForLastExpressionProps) => { + const [alertDurationPopoverOpen, setAlertDurationPopoverOpen] = useState(false); + + return ( + { + setAlertDurationPopoverOpen(true); + }} + color={timeWindowSize ? 'secondary' : 'danger'} + /> + } + isOpen={alertDurationPopoverOpen} + closePopover={() => { + setAlertDurationPopoverOpen(false); + }} + ownFocus + withTitle + anchorPosition={popupPosition ?? 'downLeft'} + > +
+ + + + + + 0 && timeWindowSize !== undefined} + error={errors.timeWindowSize} + > + 0 && timeWindowSize !== undefined} + min={1} + value={timeWindowSize} + onChange={e => { + const { value } = e.target; + const timeWindowSizeVal = value !== '' ? parseInt(value, 10) : value; + onChangeWindowSize(timeWindowSizeVal); + }} + /> + + + + { + onChangeWindowUnit(e.target.value); + }} + options={getTimeOptions(timeWindowSize)} + /> + + +
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.test.tsx new file mode 100644 index 0000000000000..39cca005d5176 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { EuiPopoverTitle } from '@elastic/eui'; +import { GroupByExpression } from './group_by_over'; + +describe('group by expression', () => { + it('renders with builtin group by types', () => { + const onChangeSelectedTermField = jest.fn(); + const onChangeSelectedGroupBy = jest.fn(); + const onChangeSelectedTermSize = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="overExpressionSelect"]')).toMatchInlineSnapshot(` + + `); + }); + + it('renders with aggregation type fields', () => { + const onChangeSelectedTermField = jest.fn(); + const onChangeSelectedGroupBy = jest.fn(); + const onChangeSelectedTermSize = jest.fn(); + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="fieldsExpressionSelect"]')).toMatchInlineSnapshot(` + + `); + }); + + it('renders with default aggreagation type preselected if no aggType was set', () => { + const onChangeSelectedTermField = jest.fn(); + const onChangeSelectedGroupBy = jest.fn(); + const onChangeSelectedTermSize = jest.fn(); + const wrapper = shallow( + + ); + wrapper.simulate('click'); + expect(wrapper.find('[value="all"]').length > 0).toBeTruthy(); + expect(wrapper.contains(over)).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx new file mode 100644 index 0000000000000..01e454187d398 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -0,0 +1,205 @@ +/* + * 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 React, { useState, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiExpression, + EuiPopover, + EuiPopoverTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiFieldNumber, +} from '@elastic/eui'; +import { builtInGroupByTypes } from '../constants'; +import { GroupByType } from '../types'; + +interface GroupByExpressionProps { + groupBy: string; + errors: { [key: string]: string[] }; + onChangeSelectedTermSize: (selectedTermSize?: number) => void; + onChangeSelectedTermField: (selectedTermField?: string) => void; + onChangeSelectedGroupBy: (selectedGroupBy?: string) => void; + fields: Record; + termSize?: number; + termField?: string; + customGroupByTypes?: { + [key: string]: GroupByType; + }; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const GroupByExpression = ({ + groupBy, + errors, + onChangeSelectedTermSize, + onChangeSelectedTermField, + onChangeSelectedGroupBy, + fields, + termSize, + termField, + customGroupByTypes, + popupPosition, +}: GroupByExpressionProps) => { + const groupByTypes = customGroupByTypes ?? builtInGroupByTypes; + const [groupByPopoverOpen, setGroupByPopoverOpen] = useState(false); + const MIN_TERM_SIZE = 1; + const firstFieldOption = { + text: i18n.translate( + 'xpack.triggersActionsUI.common.expressionItems.groupByType.timeFieldOptionLabel', + { + defaultMessage: 'Select a field', + } + ), + value: '', + }; + + return ( + { + setGroupByPopoverOpen(true); + }} + color={groupBy === 'all' || (termSize && termField) ? 'secondary' : 'danger'} + /> + } + isOpen={groupByPopoverOpen} + closePopover={() => { + setGroupByPopoverOpen(false); + }} + ownFocus + withTitle + anchorPosition={popupPosition ?? 'downRight'} + > +
+ + {i18n.translate( + 'xpack.triggersActionsUI.common.expressionItems.groupByType.overButtonLabel', + { + defaultMessage: 'over', + } + )} + + + + { + if (groupByTypes[e.target.value].sizeRequired) { + onChangeSelectedTermSize(MIN_TERM_SIZE); + onChangeSelectedTermField(''); + } else { + onChangeSelectedTermSize(undefined); + onChangeSelectedTermField(undefined); + } + onChangeSelectedGroupBy(e.target.value); + }} + options={Object.values(groupByTypes).map(({ text, value }) => { + return { + text, + value, + }; + })} + /> + + + {groupByTypes[groupBy].sizeRequired ? ( + + + 0 && termSize !== undefined} + error={errors.termSize} + > + 0 && termSize !== undefined} + value={termSize} + onChange={e => { + const { value } = e.target; + const termSizeVal = value !== '' ? parseFloat(value) : MIN_TERM_SIZE; + onChangeSelectedTermSize(termSizeVal); + }} + min={MIN_TERM_SIZE} + /> + + + + 0 && termField !== undefined} + error={errors.termField} + > + 0 && termField !== undefined} + onChange={e => { + onChangeSelectedTermField(e.target.value); + }} + options={fields.reduce( + (options: any, field: { name: string; normalizedType: string }) => { + if ( + groupByTypes[groupBy].validNormalizedTypes.includes(field.normalizedType) + ) { + options.push({ + text: field.name, + value: field.name, + }); + } + return options; + }, + [firstFieldOption] + )} + onBlur={() => { + if (termField === undefined) { + onChangeSelectedTermField(''); + } + }} + /> + + + + ) : null} + +
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts new file mode 100644 index 0000000000000..1dde99c2996cd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WhenExpression } from './when'; +export { OfExpression } from './of'; +export { GroupByExpression } from './group_by_over'; +export { ThresholdExpression } from './threshold'; +export { ForLastExpression } from './for_the_last'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx new file mode 100644 index 0000000000000..2e674f4fb47b1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx @@ -0,0 +1,124 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { EuiPopoverTitle } from '@elastic/eui'; +import { OfExpression } from './of'; + +describe('of expression', () => { + it('renders of builtin aggregation types', () => { + const onChangeSelectedAggField = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="availablefieldsOptionsComboBox"]')) + .toMatchInlineSnapshot(` + + `); + }); + + it('renders with custom aggregation types', () => { + const onChangeSelectedAggField = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="availablefieldsOptionsComboBox"]')) + .toMatchInlineSnapshot(` + + `); + }); + + it('renders with default aggreagation type preselected if no aggType was set', () => { + const onChangeSelectedAggField = jest.fn(); + const wrapper = shallow( + + ); + wrapper.simulate('click'); + expect(wrapper.contains(of)).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx new file mode 100644 index 0000000000000..954e584d52a87 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -0,0 +1,137 @@ +/* + * 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 React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiExpression, + EuiPopover, + EuiPopoverTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiComboBox, +} from '@elastic/eui'; +import { builtInAggregationTypes } from '../constants'; +import { AggregationType } from '../types'; + +interface OfExpressionProps { + aggType: string; + aggField?: string; + errors: { [key: string]: string[] }; + onChangeSelectedAggField: (selectedAggType?: string) => void; + fields: Record; + customAggTypesOptions?: { + [key: string]: AggregationType; + }; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const OfExpression = ({ + aggType, + aggField, + errors, + onChangeSelectedAggField, + fields, + customAggTypesOptions, + popupPosition, +}: OfExpressionProps) => { + const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false); + const firstFieldOption = { + text: i18n.translate( + 'xpack.triggersActionsUI.common.expressionItems.of.selectTimeFieldOptionLabel', + { + defaultMessage: 'Select a field', + } + ), + value: '', + }; + const aggregationTypes = customAggTypesOptions ?? builtInAggregationTypes; + + const availablefieldsOptions = fields.reduce((esFieldOptions: any[], field: any) => { + if (aggregationTypes[aggType].validNormalizedTypes.includes(field.normalizedType)) { + esFieldOptions.push({ + label: field.name, + }); + } + return esFieldOptions; + }, []); + + return ( + { + setAggFieldPopoverOpen(true); + }} + color={aggField ? 'secondary' : 'danger'} + /> + } + isOpen={aggFieldPopoverOpen} + closePopover={() => { + setAggFieldPopoverOpen(false); + }} + withTitle + anchorPosition={popupPosition ?? 'downRight'} + zIndex={8000} + > +
+ + {i18n.translate('xpack.triggersActionsUI.common.expressionItems.of.popoverTitle', { + defaultMessage: 'of', + })} + + + + 0 && aggField !== undefined} + error={errors.aggField} + > + 0 && aggField !== undefined} + placeholder={firstFieldOption.text} + options={availablefieldsOptions} + noSuggestions={!availablefieldsOptions.length} + selectedOptions={aggField ? [{ label: aggField }] : []} + onChange={selectedOptions => { + onChangeSelectedAggField( + selectedOptions.length === 1 ? selectedOptions[0].label : undefined + ); + setAggFieldPopoverOpen(false); + }} + /> + + + +
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx new file mode 100644 index 0000000000000..bd3c7383d4b9c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.test.tsx @@ -0,0 +1,69 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { EuiPopoverTitle } from '@elastic/eui'; +import { ThresholdExpression } from './threshold'; + +describe('threshold expression', () => { + it('renders of builtin comparators', () => { + const onChangeSelectedThreshold = jest.fn(); + const onChangeSelectedThresholdComparator = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="comparatorOptionsComboBox"]')).toMatchInlineSnapshot(` + ", + }, + Object { + "text": "Is above or equals", + "value": ">=", + }, + Object { + "text": "Is below", + "value": "<", + }, + Object { + "text": "Is below or equals", + "value": "<=", + }, + Object { + "text": "Is between", + "value": "between", + }, + ] + } + value="between" + /> + `); + }); + + it('renders with treshold title', () => { + const onChangeSelectedThreshold = jest.fn(); + const onChangeSelectedThresholdComparator = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper.contains(Is between)).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx new file mode 100644 index 0000000000000..ecbf0aee63e2d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -0,0 +1,151 @@ +/* + * 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 React, { useState, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiExpression, + EuiPopover, + EuiPopoverTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiFieldNumber, + EuiText, +} from '@elastic/eui'; +import { builtInComparators } from '../constants'; +import { Comparator } from '../types'; + +interface ThresholdExpressionProps { + thresholdComparator: string; + errors: { [key: string]: string[] }; + onChangeSelectedThresholdComparator: (selectedThresholdComparator?: string) => void; + onChangeSelectedThreshold: (selectedThreshold?: number[]) => void; + customComparators?: { + [key: string]: Comparator; + }; + threshold?: number[]; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const ThresholdExpression = ({ + thresholdComparator, + errors, + onChangeSelectedThresholdComparator, + onChangeSelectedThreshold, + customComparators, + threshold = [], + popupPosition, +}: ThresholdExpressionProps) => { + const comparators = customComparators ?? builtInComparators; + const [alertThresholdPopoverOpen, setAlertThresholdPopoverOpen] = useState(false); + + const andThresholdText = i18n.translate( + 'xpack.triggersActionsUI.common.expressionItems.threshold.andLabel', + { + defaultMessage: 'AND', + } + ); + + return ( + { + setAlertThresholdPopoverOpen(true); + }} + color={ + (errors.threshold0 && errors.threshold0.length) || + (errors.threshold1 && errors.threshold1.length) + ? 'danger' + : 'secondary' + } + /> + } + isOpen={alertThresholdPopoverOpen} + closePopover={() => { + setAlertThresholdPopoverOpen(false); + }} + ownFocus + withTitle + anchorPosition={popupPosition ?? 'downLeft'} + > +
+ {comparators[thresholdComparator].text} + + + { + onChangeSelectedThresholdComparator(e.target.value); + }} + options={Object.values(comparators).map(({ text, value }) => { + return { text, value }; + })} + /> + + {Array.from(Array(comparators[thresholdComparator].requiredValues)).map((_notUsed, i) => { + return ( + + {i > 0 ? ( + + {andThresholdText} + + ) : null} + + + { + const { value } = e.target; + const thresholdVal = value !== '' ? parseFloat(value) : undefined; + const newThreshold = [...threshold]; + if (thresholdVal) { + newThreshold[i] = thresholdVal; + } + onChangeSelectedThreshold(newThreshold); + }} + /> + + + + ); + })} + +
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx new file mode 100644 index 0000000000000..02b6bf24977c9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx @@ -0,0 +1,105 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { EuiPopoverTitle } from '@elastic/eui'; +import { WhenExpression } from './when'; + +describe('when expression', () => { + it('renders with builtin aggregation types', () => { + const onChangeSelectedAggType = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="whenExpressionSelect"]')).toMatchInlineSnapshot(` + + `); + }); + + it('renders with custom aggregation types', () => { + const onChangeSelectedAggType = jest.fn(); + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="whenExpressionSelect"]')).toMatchInlineSnapshot(` + + `); + }); + + it('renders when popover title', () => { + const onChangeSelectedAggType = jest.fn(); + const wrapper = shallow( + + ); + wrapper.simulate('click'); + expect(wrapper.find('[value="avg"]').length > 0).toBeTruthy(); + expect(wrapper.contains(when)).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx new file mode 100644 index 0000000000000..b20040608ed9e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx @@ -0,0 +1,90 @@ +/* + * 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 React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiExpression, EuiPopover, EuiPopoverTitle, EuiSelect } from '@elastic/eui'; +import { builtInAggregationTypes } from '../constants'; +import { AggregationType } from '../types'; + +interface WhenExpressionProps { + aggType: string; + customAggTypesOptions?: { [key: string]: AggregationType }; + onChangeSelectedAggType: (selectedAggType: string) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const WhenExpression = ({ + aggType, + customAggTypesOptions, + onChangeSelectedAggType, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + const aggregationTypes = customAggTypesOptions ?? builtInAggregationTypes; + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + withTitle + anchorPosition={popupPosition ?? 'downLeft'} + > +
+ + {i18n.translate('xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle', { + defaultMessage: 'when', + })} + + { + onChangeSelectedAggType(e.target.value); + setAggTypePopoverOpen(false); + }} + options={Object.values(aggregationTypes).map(({ text, value }) => { + return { + text, + value, + }; + })} + /> +
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts new file mode 100644 index 0000000000000..94089a274e79d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -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 './expression_items'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_options.test.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_options.test.ts new file mode 100644 index 0000000000000..51bacaf922420 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_options.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { getTimeOptions, getTimeFieldOptions } from './get_time_options'; + +describe('get_time_options', () => { + test('if getTimeOptions return single unit time options', () => { + const timeUnitValue = getTimeOptions(1); + expect(timeUnitValue).toMatchObject([ + { text: 'second', value: 's' }, + { text: 'minute', value: 'm' }, + { text: 'hour', value: 'h' }, + { text: 'day', value: 'd' }, + ]); + }); + + test('if getTimeOptions return multiple unit time options', () => { + const timeUnitValue = getTimeOptions(10); + expect(timeUnitValue).toMatchObject([ + { text: 'seconds', value: 's' }, + { text: 'minutes', value: 'm' }, + { text: 'hours', value: 'h' }, + { text: 'days', value: 'd' }, + ]); + }); + + test('if getTimeFieldOptions return only date type fields', () => { + const timeOnlyTypeFields = getTimeFieldOptions([ + { type: 'date', name: 'order_date' }, + { type: 'number', name: 'sum' }, + ]); + expect(timeOnlyTypeFields).toMatchObject([{ text: 'order_date', value: 'order_date' }]); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_options.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_options.ts new file mode 100644 index 0000000000000..aab29bd6ec5b0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_options.ts @@ -0,0 +1,37 @@ +/* + * 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 { getTimeUnitLabel } from './get_time_unit_label'; +import { TIME_UNITS } from '../../application/constants'; + +export const getTimeOptions = (unitSize: number) => + Object.entries(TIME_UNITS).map(([_key, value]) => { + return { + text: getTimeUnitLabel(value, unitSize.toString()), + value, + }; + }); + +interface TimeFieldOptions { + text: string; + value: string; +} + +export const getTimeFieldOptions = ( + fields: Array<{ type: string; name: string }> +): TimeFieldOptions[] => { + const options: TimeFieldOptions[] = []; + + fields.forEach((field: { type: string; name: string }) => { + if (field.type === 'date') { + options.push({ + text: field.name, + value: field.name, + }); + } + }); + return options; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_unit_label.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_unit_label.ts new file mode 100644 index 0000000000000..08aa111691786 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/get_time_unit_label.ts @@ -0,0 +1,33 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { TIME_UNITS } from '../../application/constants'; + +export function getTimeUnitLabel(timeUnit = TIME_UNITS.SECOND, timeValue = '0') { + switch (timeUnit) { + case TIME_UNITS.SECOND: + return i18n.translate('xpack.triggersActionsUI.timeUnits.secondLabel', { + defaultMessage: '{timeValue, plural, one {second} other {seconds}}', + values: { timeValue }, + }); + case TIME_UNITS.MINUTE: + return i18n.translate('xpack.triggersActionsUI.timeUnits.minuteLabel', { + defaultMessage: '{timeValue, plural, one {minute} other {minutes}}', + values: { timeValue }, + }); + case TIME_UNITS.HOUR: + return i18n.translate('xpack.triggersActionsUI.timeUnits.hourLabel', { + defaultMessage: '{timeValue, plural, one {hour} other {hours}}', + values: { timeValue }, + }); + case TIME_UNITS.DAY: + return i18n.translate('xpack.triggersActionsUI.timeUnits.dayLabel', { + defaultMessage: '{timeValue, plural, one {day} other {days}}', + values: { timeValue }, + }); + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/types.ts b/x-pack/plugins/triggers_actions_ui/public/common/types.ts new file mode 100644 index 0000000000000..fd2a401fe59f3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/types.ts @@ -0,0 +1,25 @@ +/* + * 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 interface Comparator { + text: string; + value: string; + requiredValues: number; +} + +export interface AggregationType { + text: string; + fieldRequired: boolean; + value: string; + validNormalizedTypes: string[]; +} + +export interface GroupByType { + text: string; + sizeRequired: boolean; + value: string; + validNormalizedTypes: string[]; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 7eed516019dd0..f13ed5983d0d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -7,6 +7,9 @@ import { PluginInitializerContext } from 'src/core/public'; import { Plugin } from './plugin'; +export { AlertsContextProvider } from './application/context/alerts_context'; +export { AlertAdd } from './application/sections/alert_add'; + export function plugin(ctx: PluginInitializerContext) { return new Plugin(ctx); } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 493f5462dd2f7..459197d80d7aa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -17,7 +17,11 @@ import { boot } from './application/boot'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -export type Setup = void; +export interface TriggersAndActionsUIPublicPluginSetup { + actionTypeRegistry: TypeRegistry; + alertTypeRegistry: TypeRegistry; +} + export type Start = void; interface PluginsStart { @@ -26,7 +30,7 @@ interface PluginsStart { management: ManagementStart; } -export class Plugin implements CorePlugin { +export class Plugin implements CorePlugin { private actionTypeRegistry: TypeRegistry; private alertTypeRegistry: TypeRegistry; @@ -38,7 +42,7 @@ export class Plugin implements CorePlugin { this.alertTypeRegistry = alertTypeRegistry; } - public setup(): Setup { + public setup(): TriggersAndActionsUIPublicPluginSetup { registerBuiltInActionTypes({ actionTypeRegistry: this.actionTypeRegistry, }); @@ -46,6 +50,11 @@ export class Plugin implements CorePlugin { registerBuiltInAlertTypes({ alertTypeRegistry: this.alertTypeRegistry, }); + + return { + actionTypeRegistry: this.actionTypeRegistry, + alertTypeRegistry: this.alertTypeRegistry, + }; } public start(core: CoreStart, plugins: PluginsStart) { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index c0a12144b2a18..73ecafb023848 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -83,7 +83,7 @@ export interface AlertTypeModel { id: string; name: string; iconClass: string; - validate: (alert: Alert) => ValidationResult; + validate: (alertParams: any) => ValidationResult; alertParamsExpression: React.FunctionComponent; defaultActionMessage?: string; }