[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 <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
884704d847
commit
6f3e1bfbe5
29 changed files with 475 additions and 156 deletions
|
@ -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<string, string[]>(
|
||||
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<string, string[]>(
|
||||
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)"`
|
||||
);
|
||||
});
|
||||
});
|
8
x-pack/plugins/uptime/common/lib/index.ts
Normal file
8
x-pack/plugins/uptime/common/lib/index.ts
Normal file
|
@ -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';
|
|
@ -35,6 +35,10 @@ export const stringifyKueries = (kueries: Map<string, Array<number | string>>):
|
|||
.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}`;
|
||||
}, '');
|
|
@ -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<typeof StatusCheckFiltersType>;
|
||||
|
||||
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<typeof AtomicStatusCheckParamsType>;
|
||||
|
||||
export const StatusCheckParamsType = t.intersection([
|
||||
t.partial({
|
||||
filters: t.string,
|
||||
}),
|
||||
|
@ -20,4 +43,4 @@ export const StatusCheckExecutorParamsType = t.intersection([
|
|||
}),
|
||||
]);
|
||||
|
||||
export type StatusCheckExecutorParams = t.TypeOf<typeof StatusCheckExecutorParamsType>;
|
||||
export type StatusCheckParams = t.TypeOf<typeof StatusCheckParamsType>;
|
||||
|
|
|
@ -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<AlertMonitorStatusProps> = (p
|
|||
|
||||
const [newFilters, setNewFilters] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setAlertParams('filters', filters);
|
||||
}, [filters, setAlertParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -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<Props> = ({
|
|||
timerange,
|
||||
}) => {
|
||||
const { filters, locations } = useSelector(selectMonitorStatusAlert);
|
||||
const searchText = useSelector(searchTextSelector);
|
||||
|
||||
useEffect(() => {
|
||||
setAlertParams('search', searchText);
|
||||
}, [setAlertParams, searchText]);
|
||||
|
||||
return (
|
||||
<AlertMonitorStatusComponent
|
||||
autocomplete={autocomplete}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { overviewFiltersSelector } from '../../../../state/selectors';
|
|||
import { useFilterUpdate } from '../../../../hooks/use_filter_update';
|
||||
import { filterLabels } from '../../filter_group/translations';
|
||||
import { alertFilterLabels } from './translations';
|
||||
import { StatusCheckFilters } from '../../../../../common/runtime_types';
|
||||
|
||||
interface Props {
|
||||
newFilters: string[];
|
||||
|
@ -38,19 +39,22 @@ export const FiltersExpressionsSelect: React.FC<Props> = ({
|
|||
updatedFieldValues.values
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedFieldValues.fieldName === 'observer.geo.name') {
|
||||
setAlertParams('locations', updatedFieldValues.values);
|
||||
}
|
||||
}, [setAlertParams, updatedFieldValues]);
|
||||
const [filters, setFilters] = useState<StatusCheckFilters>({
|
||||
'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 });
|
||||
};
|
||||
|
||||
|
|
|
@ -51,7 +51,8 @@ export const TimeExpressionSelect: React.FC<Props> = ({ 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 (
|
||||
|
|
|
@ -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<State>({
|
||||
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: [] });
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
22
x-pack/plugins/uptime/public/hooks/use_search_text.ts
Normal file
22
x-pack/plugins/uptime/public/hooks/use_search_text.ts
Normal file
|
@ -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 };
|
||||
};
|
|
@ -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<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, numTimes: number, timerange: { from: string, to: string } }/locations: Array<string>",
|
||||
"Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, numTimes: number, timerange: { from: string, to: string } }/numTimes: number",
|
||||
"Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> } }>)/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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> } }>)/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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> } }>)/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(`
|
||||
it('has invalid timerangeCount value', () => {
|
||||
expect(validate({ ...params, timerangeCount: 0 })).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<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, numTimes: number, timerange: { from: string, to: string } }/timerange: { from: string, to: string }",
|
||||
],
|
||||
"invalidTimeRangeValue": "Time range value must be greater than 0",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('is missing `from` or `to` value', () => {
|
||||
expect(
|
||||
validate({
|
||||
...params,
|
||||
timerange: {},
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
it('has NaN timerangeCount value', () => {
|
||||
expect(validate({ ...params, timerangeCount: NaN })).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<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, numTimes: number, timerange: { from: string, to: string } }/timerange: { from: string, to: string }/from: string",
|
||||
"Invalid value undefined supplied to : (Partial<{ filters: string }> & { locations: Array<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, 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(`
|
||||
Object {
|
||||
"errors": Object {
|
||||
"timeRangeStartValueNaN": "Specified time range \`from\` is an invalid value",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('has unparseable `to` value', () => {
|
||||
expect(
|
||||
validate({
|
||||
...params,
|
||||
timerange: {
|
||||
from: 'now-15m',
|
||||
to: 'cannot parse this to a date',
|
||||
},
|
||||
})
|
||||
).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<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> } }>)/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<string>, numTimes: number, timerange: { from: string, to: string } })/1: { locations: Array<string>, 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<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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<string, any> = {};
|
||||
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 };
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -20,6 +20,8 @@ export const setBasePath = createAction<string>('SET BASE PATH');
|
|||
|
||||
export const setEsKueryString = createAction<string>('SET ES KUERY STRING');
|
||||
|
||||
export const setSearchTextAction = createAction<string>('SET SEARCH');
|
||||
|
||||
export const toggleIntegrationsPopover = createAction<PopoverState>(
|
||||
'TOGGLE INTEGRATION POPOVER STATE'
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ Object {
|
|||
"id": "popover-2",
|
||||
"open": true,
|
||||
},
|
||||
"searchText": "",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -18,5 +19,6 @@ Object {
|
|||
"basePath": "yyz",
|
||||
"esKuery": "",
|
||||
"integrationsPopoverOpen": null,
|
||||
"searchText": "",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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<UiState, UiPayload>(
|
|||
...state,
|
||||
alertFlyoutType: action.payload,
|
||||
}),
|
||||
|
||||
[String(setSearchTextAction)]: (state, action: Action<string>) => ({
|
||||
...state,
|
||||
searchText: action.payload,
|
||||
}),
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('state selectors', () => {
|
|||
basePath: 'yyz',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
searchText: '',
|
||||
},
|
||||
monitorStatus: {
|
||||
status: null,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -23,7 +23,7 @@ export type APICaller = (
|
|||
|
||||
export type UMElasticsearchQueryFn<P, R = any> = (
|
||||
params: { callES: APICaller; dynamicSettings: DynamicSettings } & P
|
||||
) => Promise<R> | R;
|
||||
) => Promise<R>;
|
||||
|
||||
export type UMSavedObjectsQueryFn<T = any, P = undefined> = (
|
||||
client: SavedObjectsClientContract | ISavedObjectsRepository,
|
||||
|
|
|
@ -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",
|
||||
"numTimes",
|
||||
"timerange",
|
||||
"locations",
|
||||
"numTimes",
|
||||
"search",
|
||||
"timerangeCount",
|
||||
"timerangeUnit",
|
||||
"timerange",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -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(() => {
|
||||
|
|
|
@ -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<IIndexPattern | undefined>,
|
||||
filters?: StatusCheckFilters,
|
||||
search?: string
|
||||
): Promise<JsonObject | undefined> => {
|
||||
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
|
||||
|
|
|
@ -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,
|
||||
}) => {
|
||||
|
|
|
@ -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<P, R> = UMElasticsearchQueryFn<P, R>;
|
||||
|
||||
export interface UptimeRequests {
|
||||
getCerts: ESQ<GetCertsParams, CertResult>;
|
||||
getFilterBar: ESQ<GetFilterBarParams, OverviewFilters>;
|
||||
getIndexPattern: ESQ<{}, {}>;
|
||||
getIndexPattern: ESQ<{}, IIndexPattern | undefined>;
|
||||
getLatestMonitor: ESQ<GetLatestMonitorParams, Ping>;
|
||||
getMonitorDurationChart: ESQ<GetMonitorChartsParams, MonitorDurationResult>;
|
||||
getMonitorDetails: ESQ<GetMonitorDetailsParams, MonitorDetails>;
|
||||
|
|
Loading…
Reference in a new issue