From 6f3e1bfbe5bdddf9a8c404bf83e4bee3f0b2c683 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 3 Jun 2020 09:48:54 -0400 Subject: [PATCH] [Uptime] Create new atomic params type for status alerts (#67720) * Create new atomic params type for status alerts. * Update executor params typing to support both alert params types. * Update snapshot for alert factory function. * Fix broken types and refresh snapshots. * Clean up naming of action/selector. * Fix a bug and add tests. Co-authored-by: Elastic Machine --- .../stringify_kueries.test.ts.snap | 0 .../combine_filters_and_user_search.test.ts | 0 .../lib}/__tests__/stringify_kueries.test.ts | 28 +++ .../lib}/combine_filters_and_user_search.ts | 0 x-pack/plugins/uptime/common/lib/index.ts | 8 + .../lib}/stringify_kueries.ts | 4 + .../runtime_types/alerts/status_check.ts | 27 ++- .../overview/alerts/alert_monitor_status.tsx | 6 +- .../alert_monitor_status.tsx | 10 +- .../filters_expression_select.tsx | 22 +- .../time_expression_select.tsx | 3 +- .../overview/kuery_bar/kuery_bar.tsx | 11 +- x-pack/plugins/uptime/public/hooks/index.ts | 1 + .../public/hooks/update_kuery_string.ts | 2 +- .../uptime/public/hooks/use_search_text.ts | 22 ++ .../__tests__/monitor_status.test.ts | 96 ++------ .../public/lib/alert_types/monitor_status.tsx | 41 +--- .../plugins/uptime/public/lib/helper/index.ts | 2 - .../plugins/uptime/public/state/actions/ui.ts | 2 + .../__tests__/__snapshots__/ui.test.ts.snap | 2 + .../state/reducers/__tests__/ui.test.ts | 4 + .../uptime/public/state/reducers/ui.ts | 8 + .../state/selectors/__tests__/index.test.ts | 1 + .../uptime/public/state/selectors/index.ts | 2 + .../lib/adapters/framework/adapter_types.ts | 2 +- .../lib/alerts/__tests__/status_check.test.ts | 210 +++++++++++++++++- .../uptime/server/lib/alerts/status_check.ts | 110 +++++++-- .../server/lib/requests/get_index_pattern.ts | 2 +- .../server/lib/requests/uptime_requests.ts | 3 +- 29 files changed, 474 insertions(+), 155 deletions(-) rename x-pack/plugins/uptime/{public/lib/helper => common/lib}/__tests__/__snapshots__/stringify_kueries.test.ts.snap (100%) rename x-pack/plugins/uptime/{public/lib/helper => common/lib}/__tests__/combine_filters_and_user_search.test.ts (100%) rename x-pack/plugins/uptime/{public/lib/helper => common/lib}/__tests__/stringify_kueries.test.ts (69%) rename x-pack/plugins/uptime/{public/lib/helper => common/lib}/combine_filters_and_user_search.ts (100%) create mode 100644 x-pack/plugins/uptime/common/lib/index.ts rename x-pack/plugins/uptime/{public/lib/helper => common/lib}/stringify_kueries.ts (93%) create mode 100644 x-pack/plugins/uptime/public/hooks/use_search_text.ts diff --git a/x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap b/x-pack/plugins/uptime/common/lib/__tests__/__snapshots__/stringify_kueries.test.ts.snap similarity index 100% rename from x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap rename to x-pack/plugins/uptime/common/lib/__tests__/__snapshots__/stringify_kueries.test.ts.snap diff --git a/x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts b/x-pack/plugins/uptime/common/lib/__tests__/combine_filters_and_user_search.test.ts similarity index 100% rename from x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts rename to x-pack/plugins/uptime/common/lib/__tests__/combine_filters_and_user_search.test.ts diff --git a/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts b/x-pack/plugins/uptime/common/lib/__tests__/stringify_kueries.test.ts similarity index 69% rename from x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts rename to x-pack/plugins/uptime/common/lib/__tests__/stringify_kueries.test.ts index db1a846c295a0..4c32b19765427 100644 --- a/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts +++ b/x-pack/plugins/uptime/common/lib/__tests__/stringify_kueries.test.ts @@ -59,4 +59,32 @@ describe('stringifyKueries', () => { kueries.set('monitor.id', ['https://elastic.co', 'https://example.com']); expect(stringifyKueries(kueries)).toMatchSnapshot(); }); + + it('handles precending empty array', () => { + kueries = new Map( + Object.entries({ + 'monitor.type': [], + 'observer.geo.name': ['us-east', 'apj', 'sydney', 'us-west'], + tags: [], + 'url.port': [], + }) + ); + expect(stringifyKueries(kueries)).toMatchInlineSnapshot( + `"(observer.geo.name:us-east or observer.geo.name:apj or observer.geo.name:sydney or observer.geo.name:us-west)"` + ); + }); + + it('handles skipped empty arrays', () => { + kueries = new Map( + Object.entries({ + tags: [], + 'monitor.type': ['http'], + 'url.port': [], + 'observer.geo.name': ['us-east', 'apj', 'sydney', 'us-west'], + }) + ); + expect(stringifyKueries(kueries)).toMatchInlineSnapshot( + `"monitor.type:http and (observer.geo.name:us-east or observer.geo.name:apj or observer.geo.name:sydney or observer.geo.name:us-west)"` + ); + }); }); diff --git a/x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts b/x-pack/plugins/uptime/common/lib/combine_filters_and_user_search.ts similarity index 100% rename from x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts rename to x-pack/plugins/uptime/common/lib/combine_filters_and_user_search.ts diff --git a/x-pack/plugins/uptime/common/lib/index.ts b/x-pack/plugins/uptime/common/lib/index.ts new file mode 100644 index 0000000000000..2daec0adf87e4 --- /dev/null +++ b/x-pack/plugins/uptime/common/lib/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './combine_filters_and_user_search'; +export * from './stringify_kueries'; diff --git a/x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts b/x-pack/plugins/uptime/common/lib/stringify_kueries.ts similarity index 93% rename from x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts rename to x-pack/plugins/uptime/common/lib/stringify_kueries.ts index d34ee9665de33..490fd1661f782 100644 --- a/x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts +++ b/x-pack/plugins/uptime/common/lib/stringify_kueries.ts @@ -35,6 +35,10 @@ export const stringifyKueries = (kueries: Map>): .reduce((prev, cur, index, array) => { if (array.length === 1 || index === 0) { return cur; + } else if (cur === '') { + return prev; + } else if (prev === '' && !!cur) { + return cur; } return `${prev} and ${cur}`; }, ''); diff --git a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts index 909669bb5d3eb..74d5337256601 100644 --- a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts +++ b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts @@ -6,7 +6,30 @@ import * as t from 'io-ts'; -export const StatusCheckExecutorParamsType = t.intersection([ +export const StatusCheckFiltersType = t.type({ + 'monitor.type': t.array(t.string), + 'observer.geo.name': t.array(t.string), + tags: t.array(t.string), + 'url.port': t.array(t.string), +}); + +export type StatusCheckFilters = t.TypeOf; + +export const AtomicStatusCheckParamsType = t.intersection([ + t.type({ + numTimes: t.number, + timerangeCount: t.number, + timerangeUnit: t.string, + }), + t.partial({ + search: t.string, + filters: StatusCheckFiltersType, + }), +]); + +export type AtomicStatusCheckParams = t.TypeOf; + +export const StatusCheckParamsType = t.intersection([ t.partial({ filters: t.string, }), @@ -20,4 +43,4 @@ export const StatusCheckExecutorParamsType = t.intersection([ }), ]); -export type StatusCheckExecutorParams = t.TypeOf; +export type StatusCheckParams = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx index 62f92fe8a5142..e2e44124ec659 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { DataPublicPluginSetup } from 'src/plugins/data/public'; import * as labels from './translations'; @@ -35,10 +35,6 @@ export const AlertMonitorStatusComponent: React.FC = (p const [newFilters, setNewFilters] = useState([]); - useEffect(() => { - setAlertParams('filters', filters); - }, [filters, setAlertParams]); - return ( <> diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx index 77e0c98c0260d..973a3e1d477b6 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { DataPublicPluginSetup } from 'src/plugins/data/public'; -import { selectMonitorStatusAlert } from '../../../../state/selectors'; +import { selectMonitorStatusAlert, searchTextSelector } from '../../../../state/selectors'; import { AlertMonitorStatusComponent } from '../index'; interface Props { @@ -29,6 +29,12 @@ export const AlertMonitorStatus: React.FC = ({ timerange, }) => { const { filters, locations } = useSelector(selectMonitorStatusAlert); + const searchText = useSelector(searchTextSelector); + + useEffect(() => { + setAlertParams('search', searchText); + }, [setAlertParams, searchText]); + return ( = ({ updatedFieldValues.values ); - useEffect(() => { - if (updatedFieldValues.fieldName === 'observer.geo.name') { - setAlertParams('locations', updatedFieldValues.values); - } - }, [setAlertParams, updatedFieldValues]); + const [filters, setFilters] = useState({ + 'observer.geo.name': selectedLocations, + 'url.port': selectedPorts, + tags: selectedTags, + 'monitor.type': selectedSchemes, + }); useEffect(() => { - setAlertParams('locations', []); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + setAlertParams('filters', filters); + }, [filters, setAlertParams]); const onFilterFieldChange = (fieldName: string, values: string[]) => { + setFilters({ + ...filters, + [fieldName]: values, + }); setUpdatedFieldValues({ fieldName, values }); }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx index aabc6fd6e6623..e3893845862fb 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx @@ -51,7 +51,8 @@ export const TimeExpressionSelect: React.FC = ({ setAlertParams }) => { useEffect(() => { const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm'; - setAlertParams('timerange', { from: `now-${numUnits}${timerangeUnit}`, to: 'now' }); + setAlertParams('timerangeUnit', timerangeUnit); + setAlertParams('timerangeCount', numUnits); }, [numUnits, timerangeUnitOptions, setAlertParams]); return ( diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx index a63dbfdecef42..5c0ee632a2bda 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { uniqueId, startsWith } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { Typeahead } from './typeahead'; -import { useUrlParams } from '../../../hooks'; +import { useSearchText, useUrlParams } from '../../../hooks'; import { esKuery, IIndexPattern, @@ -45,6 +45,7 @@ export function KueryBar({ 'data-test-subj': dataTestSubj, }: Props) { const { loading, index_pattern: indexPattern } = useIndexPattern(); + const { updateSearchText } = useSearchText(); const [state, setState] = useState({ suggestions: [], @@ -56,6 +57,10 @@ export function KueryBar({ const [getUrlParams, updateUrlParams] = useUrlParams(); const { search: kuery } = getUrlParams(); + useEffect(() => { + updateSearchText(kuery); + }, [kuery, updateSearchText]); + const indexPatternMissing = loading && !indexPattern; async function onChange(inputValue: string, selectionStart: number) { @@ -63,6 +68,8 @@ export function KueryBar({ return; } + updateSearchText(inputValue); + setIsLoadingSuggestions(true); setState({ ...state, suggestions: [] }); diff --git a/x-pack/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts index b92d2d4cf7df5..14264710f7a0d 100644 --- a/x-pack/plugins/uptime/public/hooks/index.ts +++ b/x-pack/plugins/uptime/public/hooks/index.ts @@ -9,3 +9,4 @@ export * from './use_url_params'; export * from './use_telemetry'; export * from './update_kuery_string'; export * from './use_cert_status'; +export * from './use_search_text'; diff --git a/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts index 492d2bab5bb80..8a9e134e98bfd 100644 --- a/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineFiltersAndUserSearch, stringifyKueries } from '../lib/helper'; import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; +import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib'; const getKueryString = (urlFilters: string): string => { let kueryString = ''; diff --git a/x-pack/plugins/uptime/public/hooks/use_search_text.ts b/x-pack/plugins/uptime/public/hooks/use_search_text.ts new file mode 100644 index 0000000000000..8226c2365b767 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_search_text.ts @@ -0,0 +1,22 @@ +/* + * 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 { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { setSearchTextAction } from '../state/actions'; +import { searchTextSelector } from '../state/selectors'; + +export const useSearchText = () => { + const dispatch = useDispatch(); + const searchText = useSelector(searchTextSelector); + + const updateSearchText = useCallback( + (nextSearchText: string) => dispatch(setSearchTextAction(nextSearchText)), + [dispatch] + ); + + return { searchText, updateSearchText }; +}; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts index b06a7cc93f628..098a999b0d89c 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts @@ -12,12 +12,9 @@ describe('monitor status alert type', () => { beforeEach(() => { params = { - locations: [], numTimes: 5, - timerange: { - from: 'now-15m', - to: 'now', - }, + timerangeCount: 15, + timerangeUnit: 'm', }; }); @@ -27,9 +24,9 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/locations: Array", - "Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/numTimes: number", - "Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/timerange: { from: string, to: string }", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string", ], }, } @@ -37,88 +34,21 @@ describe('monitor status alert type', () => { }); describe('timerange', () => { - it('is undefined', () => { - delete params.timerange; - expect(validate(params)).toMatchInlineSnapshot(` - Object { - "errors": Object { - "typeCheckFailure": "Provided parameters do not conform to the expected type.", - "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/timerange: { from: string, to: string }", - ], - }, - } - `); - }); - - it('is missing `from` or `to` value', () => { - expect( - validate({ - ...params, - timerange: {}, - }) - ).toMatchInlineSnapshot(` - Object { - "errors": Object { - "typeCheckFailure": "Provided parameters do not conform to the expected type.", - "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/timerange: { from: string, to: string }/from: string", - "Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/timerange: { from: string, to: string }/to: string", - ], - }, - } - `); - }); - - it('is invalid timespan', () => { - expect( - validate({ - ...params, - timerange: { - from: 'now', - to: 'now-15m', - }, - }) - ).toMatchInlineSnapshot(` - Object { - "errors": Object { - "invalidTimeRange": "Time range start cannot exceed time range end", - }, - } - `); - }); - - it('has unparseable `from` value', () => { - expect( - validate({ - ...params, - timerange: { - from: 'cannot parse this to a date', - to: 'now', - }, - }) - ).toMatchInlineSnapshot(` + it('has invalid timerangeCount value', () => { + expect(validate({ ...params, timerangeCount: 0 })).toMatchInlineSnapshot(` Object { "errors": Object { - "timeRangeStartValueNaN": "Specified time range \`from\` is an invalid value", + "invalidTimeRangeValue": "Time range value must be greater than 0", }, } `); }); - it('has unparseable `to` value', () => { - expect( - validate({ - ...params, - timerange: { - from: 'now-15m', - to: 'cannot parse this to a date', - }, - }) - ).toMatchInlineSnapshot(` + it('has NaN timerangeCount value', () => { + expect(validate({ ...params, timerangeCount: NaN })).toMatchInlineSnapshot(` Object { "errors": Object { - "timeRangeEndValueNaN": "Specified time range \`to\` is an invalid value", + "timeRangeStartValueNaN": "Specified time range value must be a number", }, } `); @@ -133,7 +63,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } @@ -146,7 +76,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value \\"this isn't a number\\" supplied to : (Partial<{ filters: string }> & { locations: Array, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array, numTimes: number, timerange: { from: string, to: string } }/numTimes: number", + "Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 08fc044bee201..a39317f8db1ed 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -4,51 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; import React from 'react'; -import DateMath from '@elastic/datemath'; import { isRight } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { AlertTypeInitializer } from '.'; -import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; +import { AtomicStatusCheckParamsType } from '../../../common/runtime_types'; import { MonitorStatusTitle } from './monitor_status_title'; import { CLIENT_ALERT_TYPES } from '../../../common/constants'; import { MonitorStatusTranslations } from './translations'; -export const validate = (alertParams: any) => { +export const validate = (alertParams: unknown) => { const errors: Record = {}; - const decoded = StatusCheckExecutorParamsType.decode(alertParams); + const decoded = AtomicStatusCheckParamsType.decode(alertParams); - /* - * When the UI initially loads, this validate function is called with an - * empty set of params, we don't want to type check against that. - */ if (!isRight(decoded)) { errors.typeCheckFailure = 'Provided parameters do not conform to the expected type.'; errors.typeCheckParsingMessage = PathReporter.report(decoded); - } - - if (isRight(decoded)) { - const { numTimes, timerange } = decoded.right; - const { from, to } = timerange; - const fromAbs = DateMath.parse(from)?.valueOf(); - const toAbs = DateMath.parse(to)?.valueOf(); - if (!fromAbs || isNaN(fromAbs)) { - errors.timeRangeStartValueNaN = 'Specified time range `from` is an invalid value'; - } - if (!toAbs || isNaN(toAbs)) { - errors.timeRangeEndValueNaN = 'Specified time range `to` is an invalid value'; - } - - // the default values for this test will pass, we only want to specify an error - // in the case that `from` is more recent than `to` - if ((fromAbs ?? 0) > (toAbs ?? 1)) { - errors.invalidTimeRange = 'Time range start cannot exceed time range end'; - } - + } else { + const { numTimes, timerangeCount } = decoded.right; if (numTimes < 1) { errors.invalidNumTimes = 'Number of alert check down times must be an integer greater than 0'; } + if (isNaN(timerangeCount)) { + errors.timeRangeStartValueNaN = 'Specified time range value must be a number'; + } + if (timerangeCount <= 0) { + errors.invalidTimeRangeValue = 'Time range value must be greater than 0'; + } } return { errors }; diff --git a/x-pack/plugins/uptime/public/lib/helper/index.ts b/x-pack/plugins/uptime/public/lib/helper/index.ts index e2aa4a2b3d429..cf49328141b83 100644 --- a/x-pack/plugins/uptime/public/lib/helper/index.ts +++ b/x-pack/plugins/uptime/public/lib/helper/index.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { combineFiltersAndUserSearch } from './combine_filters_and_user_search'; export { convertMicrosecondsToMilliseconds } from './convert_measurements'; export * from './observability_integration'; export { getChartDateLabel } from './charts'; export { seriesHasDownValues } from './series_has_down_values'; -export { stringifyKueries } from './stringify_kueries'; export { UptimeUrlParams, getSupportedUrlParams } from './url_params'; diff --git a/x-pack/plugins/uptime/public/state/actions/ui.ts b/x-pack/plugins/uptime/public/state/actions/ui.ts index 0d21e177d3e40..04ad6c2fa0bf3 100644 --- a/x-pack/plugins/uptime/public/state/actions/ui.ts +++ b/x-pack/plugins/uptime/public/state/actions/ui.ts @@ -20,6 +20,8 @@ export const setBasePath = createAction('SET BASE PATH'); export const setEsKueryString = createAction('SET ES KUERY STRING'); +export const setSearchTextAction = createAction('SET SEARCH'); + export const toggleIntegrationsPopover = createAction( 'TOGGLE INTEGRATION POPOVER STATE' ); diff --git a/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap index f8faf78fbc504..c11b146101d35 100644 --- a/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap +++ b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap @@ -9,6 +9,7 @@ Object { "id": "popover-2", "open": true, }, + "searchText": "", } `; @@ -18,5 +19,6 @@ Object { "basePath": "yyz", "esKuery": "", "integrationsPopoverOpen": null, + "searchText": "", } `; diff --git a/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts index 94bc626088c84..3b8447ec2d713 100644 --- a/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts +++ b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts @@ -18,6 +18,7 @@ describe('ui reducer', () => { basePath: 'abc', esKuery: '', integrationsPopoverOpen: null, + searchText: '', }, action ) @@ -36,6 +37,7 @@ describe('ui reducer', () => { basePath: '', esKuery: '', integrationsPopoverOpen: null, + searchText: '', }, action ) @@ -51,6 +53,7 @@ describe('ui reducer', () => { basePath: '', esKuery: '', integrationsPopoverOpen: null, + searchText: '', }, action ) @@ -60,6 +63,7 @@ describe('ui reducer', () => { "basePath": "", "esKuery": "", "integrationsPopoverOpen": null, + "searchText": "", } `); }); diff --git a/x-pack/plugins/uptime/public/state/reducers/ui.ts b/x-pack/plugins/uptime/public/state/reducers/ui.ts index 9e7bc2ad02723..3cf4ae9c0bbf2 100644 --- a/x-pack/plugins/uptime/public/state/reducers/ui.ts +++ b/x-pack/plugins/uptime/public/state/reducers/ui.ts @@ -13,6 +13,7 @@ import { UiPayload, setAlertFlyoutType, setAlertFlyoutVisible, + setSearchTextAction, } from '../actions'; export interface UiState { @@ -20,6 +21,7 @@ export interface UiState { alertFlyoutType?: string; basePath: string; esKuery: string; + searchText: string; integrationsPopoverOpen: PopoverState | null; } @@ -27,6 +29,7 @@ const initialState: UiState = { alertFlyoutVisible: false, basePath: '', esKuery: '', + searchText: '', integrationsPopoverOpen: null, }; @@ -56,6 +59,11 @@ export const uiReducer = handleActions( ...state, alertFlyoutType: action.payload, }), + + [String(setSearchTextAction)]: (state, action: Action) => ({ + ...state, + searchText: action.payload, + }), }, initialState ); diff --git a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts index d8121e29d0cae..2eb0f1e8cb0ee 100644 --- a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -44,6 +44,7 @@ describe('state selectors', () => { basePath: 'yyz', esKuery: '', integrationsPopoverOpen: null, + searchText: '', }, monitorStatus: { status: null, diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts index ce295faaf5763..b088c346ad811 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -84,3 +84,5 @@ export const monitorListSelector = ({ monitorList }: AppState) => monitorList; export const overviewFiltersSelector = ({ overviewFilters }: AppState) => overviewFilters; export const esKuerySelector = ({ ui: { esKuery } }: AppState) => esKuery; + +export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchText; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 5ffc71945caef..8d26838811262 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -23,7 +23,7 @@ export type APICaller = ( export type UMElasticsearchQueryFn = ( params: { callES: APICaller; dynamicSettings: DynamicSettings } & P -) => Promise | R; +) => Promise; export type UMSavedObjectsQueryFn = ( client: SavedObjectsClientContract | ISavedObjectsRepository, diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 8c487c85c5720..6cd836525c077 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -6,9 +6,11 @@ import { contextMessage, - uniqueMonitorIds, - statusCheckAlertFactory, fullListByIdAndLocation, + genFilterString, + hasFilters, + statusCheckAlertFactory, + uniqueMonitorIds, } from '../status_check'; import { GetMonitorStatusResult } from '../../requests'; import { AlertType } from '../../../../../alerts/server'; @@ -310,9 +312,12 @@ describe('status check alert', () => { expect(Object.keys(alert.validate?.params?.props ?? {})).toMatchInlineSnapshot(` Array [ "filters", + "locations", "numTimes", + "search", + "timerangeCount", + "timerangeUnit", "timerange", - "locations", ] `); }); @@ -332,6 +337,205 @@ describe('status check alert', () => { }); }); + describe('hasFilters', () => { + it('returns false for undefined filters', () => { + expect(hasFilters()).toBe(false); + }); + + it('returns false for empty filters', () => { + expect( + hasFilters({ + 'monitor.type': [], + 'observer.geo.name': [], + tags: [], + 'url.port': [], + }) + ).toBe(false); + }); + + it('returns true for an object with a filter', () => { + expect( + hasFilters({ + 'monitor.type': [], + 'observer.geo.name': ['us-east', 'us-west'], + tags: [], + 'url.port': [], + }) + ).toBe(true); + }); + }); + + describe('genFilterString', () => { + const mockGetIndexPattern = jest.fn(); + mockGetIndexPattern.mockReturnValue(undefined); + + it('returns `undefined` for no filters or search', async () => { + expect(await genFilterString(mockGetIndexPattern)).toBeUndefined(); + }); + + it('creates a filter string for filters only', async () => { + const res = await genFilterString(mockGetIndexPattern, { + 'monitor.type': [], + 'observer.geo.name': ['us-east', 'us-west'], + tags: [], + 'url.port': [], + }); + expect(res).toMatchInlineSnapshot(` + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "us-east", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "us-west", + }, + }, + ], + }, + }, + ], + }, + } + `); + }); + + it('creates a filter string for search only', async () => { + expect(await genFilterString(mockGetIndexPattern, undefined, 'monitor.id: "kibana-dev"')) + .toMatchInlineSnapshot(` + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "kibana-dev", + }, + }, + ], + }, + } + `); + }); + + it('creates a filter string for filters and string', async () => { + const res = await genFilterString( + mockGetIndexPattern, + { + 'monitor.type': [], + 'observer.geo.name': ['us-east', 'apj', 'sydney', 'us-west'], + tags: [], + 'url.port': [], + }, + 'monitor.id: "kibana-dev"' + ); + expect(res).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "us-east", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "apj", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "sydney", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "us-west", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "kibana-dev", + }, + }, + ], + }, + }, + ], + }, + } + `); + }); + }); + describe('uniqueMonitorIds', () => { let items: GetMonitorStatusResult[]; beforeEach(() => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 3dd1558f5da91..cd42082b42c84 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -11,11 +11,19 @@ import { i18n } from '@kbn/i18n'; import { AlertExecutorOptions } from '../../../../alerts/server'; import { UptimeAlertTypeFactory } from './types'; import { GetMonitorStatusResult } from '../requests'; -import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; +import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common'; +import { + StatusCheckParamsType, + StatusCheckParams, + StatusCheckFilters, + AtomicStatusCheckParamsType, +} from '../../../common/runtime_types'; import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; import { savedObjectsAdapter } from '../saved_objects'; import { updateState } from './common'; import { commonStateTranslations } from './translations'; +import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; @@ -124,6 +132,44 @@ export const fullListByIdAndLocation = ( // we might want to make this a parameter in the future const DEFAULT_MAX_MESSAGE_ROWS = 3; +export const hasFilters = (filters?: StatusCheckFilters) => { + if (!filters) return false; + for (const list of Object.values(filters)) { + if (list.length > 0) { + return true; + } + } + return false; +}; + +export const genFilterString = async ( + getIndexPattern: () => Promise, + filters?: StatusCheckFilters, + search?: string +): Promise => { + const filtersExist = hasFilters(filters); + if (!filtersExist && !search) return undefined; + + let filterString: string | undefined; + if (filtersExist) { + filterString = stringifyKueries(new Map(Object.entries(filters ?? {}))); + } + + let combinedString: string | undefined; + if (filterString && search) { + combinedString = combineFiltersAndUserSearch(filterString, search); + } else if (filterString) { + combinedString = filterString; + } else if (search) { + combinedString = search; + } + + return esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(combinedString ?? ''), + await getIndexPattern() + ); +}; + export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ id: 'xpack.uptime.alerts.monitorStatus', name: i18n.translate('xpack.uptime.alerts.monitorStatus', { @@ -131,13 +177,28 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = }), validate: { params: schema.object({ - filters: schema.maybe(schema.string()), + filters: schema.maybe( + schema.oneOf([ + schema.object({ + 'monitor.type': schema.maybe(schema.arrayOf(schema.string())), + 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())), + tags: schema.maybe(schema.arrayOf(schema.string())), + 'url.port': schema.maybe(schema.arrayOf(schema.string())), + }), + schema.string(), + ]) + ), + locations: schema.maybe(schema.arrayOf(schema.string())), numTimes: schema.number(), - timerange: schema.object({ - from: schema.string(), - to: schema.string(), - }), - locations: schema.arrayOf(schema.string()), + search: schema.maybe(schema.string()), + timerangeCount: schema.maybe(schema.number()), + timerangeUnit: schema.maybe(schema.string()), + timerange: schema.maybe( + schema.object({ + from: schema.string(), + to: schema.string(), + }) + ), }), }, defaultActionGroupId: MONITOR_STATUS.id, @@ -174,18 +235,41 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = producer: 'uptime', async executor(options: AlertExecutorOptions) { const { params: rawParams } = options; - const decoded = StatusCheckExecutorParamsType.decode(rawParams); - if (!isRight(decoded)) { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + options.services.savedObjectsClient + ); + const atomicDecoded = AtomicStatusCheckParamsType.decode(rawParams); + const decoded = StatusCheckParamsType.decode(rawParams); + let params: StatusCheckParams; + if (isRight(atomicDecoded)) { + const { filters, search, numTimes, timerangeCount, timerangeUnit } = atomicDecoded.right; + const timerange = { from: `now-${String(timerangeCount) + timerangeUnit}`, to: 'now' }; + const filterString = JSON.stringify( + await genFilterString( + () => + libs.requests.getIndexPattern({ + callES: options.services.callCluster, + dynamicSettings, + }), + filters, + search + ) + ); + params = { + timerange, + numTimes, + locations: [], + filters: filterString, + }; + } else if (isRight(decoded)) { + params = decoded.right; + } else { ThrowReporter.report(decoded); return { error: 'Alert param types do not conform to required shape.', }; } - const params = decoded.right; - const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( - options.services.savedObjectsClient - ); /* This is called `monitorsByLocation` but it's really * monitors by location by status. The query we run to generate this * filters on the status field, so effectively there should be one and only one diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts index 7902d9a5c8536..7b08752f12249 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -8,7 +8,7 @@ import { APICaller, CallAPIOptions } from 'src/core/server'; import { UMElasticsearchQueryFn } from '../adapters'; import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../src/plugins/data/server'; -export const getUptimeIndexPattern: UMElasticsearchQueryFn<{}, {}> = async ({ +export const getUptimeIndexPattern: UMElasticsearchQueryFn<{}, IIndexPattern | undefined> = async ({ callES, dynamicSettings, }) => { diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 367db924cf1c6..85fc2c3ef9771 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -33,13 +33,14 @@ import { } from '.'; import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; +import { IIndexPattern } from '../../../../../../src/plugins/data/server'; type ESQ = UMElasticsearchQueryFn; export interface UptimeRequests { getCerts: ESQ; getFilterBar: ESQ; - getIndexPattern: ESQ<{}, {}>; + getIndexPattern: ESQ<{}, IIndexPattern | undefined>; getLatestMonitor: ESQ; getMonitorDurationChart: ESQ; getMonitorDetails: ESQ;