mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Synthetics] Add logical AND to monitor tags and locations filter (#217985)
This commit is contained in:
parent
7ac6488a0e
commit
061b93093e
21 changed files with 415 additions and 78 deletions
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const useLogicalAndFields = ['tags', 'locations'] as const;
|
||||||
|
|
||||||
|
export type UseLogicalAndField = (typeof useLogicalAndFields)[number];
|
||||||
|
|
||||||
|
export const isLogicalAndField = (field: string): field is UseLogicalAndField => {
|
||||||
|
return Object.values<string>(useLogicalAndFields).includes(field);
|
||||||
|
};
|
|
@ -11,3 +11,4 @@ export * from './capabilities';
|
||||||
export * from './settings_defaults';
|
export * from './settings_defaults';
|
||||||
export * from './ui';
|
export * from './ui';
|
||||||
export * from './synthetics';
|
export * from './synthetics';
|
||||||
|
export * from './filters_fields_with_logical_and';
|
||||||
|
|
|
@ -6,12 +6,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as t from 'io-ts';
|
import * as t from 'io-ts';
|
||||||
|
import { Mixed } from 'io-ts';
|
||||||
|
import { useLogicalAndFields } from '../../constants/filters_fields_with_logical_and';
|
||||||
|
|
||||||
export const FetchMonitorManagementListQueryArgsCodec = t.partial({
|
const useLogicalAndFileLiteral = useLogicalAndFields.map((f) => t.literal(f)) as unknown as [
|
||||||
page: t.number,
|
Mixed,
|
||||||
perPage: t.number,
|
Mixed,
|
||||||
sortField: t.string,
|
...Mixed[]
|
||||||
sortOrder: t.union([t.literal('desc'), t.literal('asc')]),
|
];
|
||||||
|
|
||||||
|
const FetchMonitorQueryArgsCommon = {
|
||||||
query: t.string,
|
query: t.string,
|
||||||
searchFields: t.array(t.string),
|
searchFields: t.array(t.string),
|
||||||
tags: t.array(t.string),
|
tags: t.array(t.string),
|
||||||
|
@ -20,8 +24,17 @@ export const FetchMonitorManagementListQueryArgsCodec = t.partial({
|
||||||
projects: t.array(t.string),
|
projects: t.array(t.string),
|
||||||
schedules: t.array(t.string),
|
schedules: t.array(t.string),
|
||||||
monitorQueryIds: t.array(t.string),
|
monitorQueryIds: t.array(t.string),
|
||||||
internal: t.boolean,
|
sortField: t.string,
|
||||||
|
sortOrder: t.union([t.literal('desc'), t.literal('asc')]),
|
||||||
showFromAllSpaces: t.boolean,
|
showFromAllSpaces: t.boolean,
|
||||||
|
useLogicalAndFor: t.array(t.union(useLogicalAndFileLiteral)),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FetchMonitorManagementListQueryArgsCodec = t.partial({
|
||||||
|
...FetchMonitorQueryArgsCommon,
|
||||||
|
page: t.number,
|
||||||
|
perPage: t.number,
|
||||||
|
internal: t.boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FetchMonitorManagementListQueryArgs = t.TypeOf<
|
export type FetchMonitorManagementListQueryArgs = t.TypeOf<
|
||||||
|
@ -29,17 +42,7 @@ export type FetchMonitorManagementListQueryArgs = t.TypeOf<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const FetchMonitorOverviewQueryArgsCodec = t.partial({
|
export const FetchMonitorOverviewQueryArgsCodec = t.partial({
|
||||||
query: t.string,
|
...FetchMonitorQueryArgsCommon,
|
||||||
searchFields: t.array(t.string),
|
|
||||||
tags: t.array(t.string),
|
|
||||||
locations: t.array(t.string),
|
|
||||||
projects: t.array(t.string),
|
|
||||||
schedules: t.array(t.string),
|
|
||||||
monitorTypes: t.array(t.string),
|
|
||||||
monitorQueryIds: t.array(t.string),
|
|
||||||
sortField: t.string,
|
|
||||||
sortOrder: t.string,
|
|
||||||
showFromAllSpaces: t.boolean,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FetchMonitorOverviewQueryArgs = t.TypeOf<typeof FetchMonitorOverviewQueryArgsCodec>;
|
export type FetchMonitorOverviewQueryArgs = t.TypeOf<typeof FetchMonitorOverviewQueryArgsCodec>;
|
||||||
|
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { before, expect, journey, step, after } from '@elastic/synthetics';
|
||||||
|
import { RetryService } from '@kbn/ftr-common-functional-services';
|
||||||
|
import { syntheticsAppPageProvider } from '../page_objects/synthetics_app';
|
||||||
|
import { SyntheticsServices } from './services/synthetics_services';
|
||||||
|
|
||||||
|
const FIRST_TAG = 'a';
|
||||||
|
const SECOND_TAG = 'b';
|
||||||
|
|
||||||
|
journey('FilterMonitors', async ({ page, params }) => {
|
||||||
|
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params });
|
||||||
|
const syntheticsService = new SyntheticsServices(params);
|
||||||
|
const retry: RetryService = params.getService('retry');
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await syntheticsService.cleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await syntheticsService.cleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
step('Go to Monitors overview page', async () => {
|
||||||
|
await syntheticsApp.navigateToOverview(true, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
step('Create test monitors', async () => {
|
||||||
|
const common = { type: 'http', urls: 'https://www.google.com', locations: ['us_central'] };
|
||||||
|
await syntheticsService.addTestMonitor('Test Filter Monitors 1 Tag', {
|
||||||
|
...common,
|
||||||
|
tags: [FIRST_TAG],
|
||||||
|
});
|
||||||
|
await syntheticsService.addTestMonitor('Test Filter Monitors 2 Tags', {
|
||||||
|
...common,
|
||||||
|
tags: [FIRST_TAG, SECOND_TAG],
|
||||||
|
});
|
||||||
|
await page.getByTestId('syntheticsRefreshButtonButton').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
step('Filter monitors by tags: use logical AND', async () => {
|
||||||
|
let requestMade = false;
|
||||||
|
page.on('request', (request) => {
|
||||||
|
if (
|
||||||
|
request
|
||||||
|
.url()
|
||||||
|
.includes(`synthetics/overview_status?query=&tags=${FIRST_TAG}&tags=${SECOND_TAG}`) &&
|
||||||
|
request.url().includes('useLogicalAndFor=tags') &&
|
||||||
|
request.method() === 'GET'
|
||||||
|
) {
|
||||||
|
requestMade = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on the Tags filter button using aria-label
|
||||||
|
await page.getByLabel('expands filter group for Tags filter').click();
|
||||||
|
|
||||||
|
// Click on both tags and on the logical AND switch
|
||||||
|
await page.getByRole('option', { name: FIRST_TAG }).click();
|
||||||
|
await page.getByRole('option', { name: SECOND_TAG }).click();
|
||||||
|
await page.getByTestId('tagsLogicalOperatorSwitch').click();
|
||||||
|
await page.getByTestId('o11yFieldValueSelectionApplyButton').click();
|
||||||
|
|
||||||
|
await retry.tryForTime(5 * 1000, async () => {
|
||||||
|
expect(requestMade).toBe(true);
|
||||||
|
// Only one monitor should be shown because we are using logical AND
|
||||||
|
await expect(page.getByText('Showing 1 Monitor')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
step('Filter monitors by tags: use logical OR', async () => {
|
||||||
|
let requestMade = false;
|
||||||
|
page.on('request', (request) => {
|
||||||
|
if (
|
||||||
|
request
|
||||||
|
.url()
|
||||||
|
.includes(`synthetics/overview_status?query=&tags=${FIRST_TAG}&tags=${SECOND_TAG}`) &&
|
||||||
|
request.method() === 'GET'
|
||||||
|
) {
|
||||||
|
requestMade = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on the Tags filter button using aria-label
|
||||||
|
await page.getByLabel('expands filter group for Tags filter').click();
|
||||||
|
|
||||||
|
// Turn off the logical AND switch
|
||||||
|
await page.getByTestId('tagsLogicalOperatorSwitch').click();
|
||||||
|
await page.getByTestId('o11yFieldValueSelectionApplyButton').click();
|
||||||
|
|
||||||
|
await retry.tryForTime(5 * 1000, async () => {
|
||||||
|
expect(requestMade).toBe(true);
|
||||||
|
// Two monitors should be shown because we are using logical OR
|
||||||
|
await expect(page.getByText('Showing 2 Monitors')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -27,3 +27,4 @@ export * from './test_run_details.journey';
|
||||||
export * from './step_details.journey';
|
export * from './step_details.journey';
|
||||||
export * from './project_monitor_read_only.journey';
|
export * from './project_monitor_read_only.journey';
|
||||||
export * from './overview_save_lens_visualization.journey';
|
export * from './overview_save_lens_visualization.journey';
|
||||||
|
export * from './filter_monitors.journey';
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FieldValueSelection } from '@kbn/observability-shared-plugin/public';
|
import { FieldValueSelection } from '@kbn/observability-shared-plugin/public';
|
||||||
|
import { isLogicalAndField } from '../../../../../../../common/constants';
|
||||||
import {
|
import {
|
||||||
getSyntheticsFilterDisplayValues,
|
getSyntheticsFilterDisplayValues,
|
||||||
SyntheticsMonitorFilterItem,
|
SyntheticsMonitorFilterItem,
|
||||||
|
@ -37,6 +38,8 @@ export const FilterButton = ({
|
||||||
[]
|
[]
|
||||||
).map(({ label: selectedValueLabel }) => selectedValueLabel);
|
).map(({ label: selectedValueLabel }) => selectedValueLabel);
|
||||||
|
|
||||||
|
const showLogicalConditionSwitch = isLogicalAndField(field);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldValueSelection
|
<FieldValueSelection
|
||||||
selectedValue={selectedValueLabels}
|
selectedValue={selectedValueLabels}
|
||||||
|
@ -48,10 +51,14 @@ export const FilterButton = ({
|
||||||
: values
|
: values
|
||||||
}
|
}
|
||||||
setQuery={setQuery}
|
setQuery={setQuery}
|
||||||
onChange={(selectedValues) => handleFilterChange(field, selectedValues)}
|
onChange={(selectedValues, _, isLogicalAND) =>
|
||||||
|
handleFilterChange(field, selectedValues, isLogicalAND)
|
||||||
|
}
|
||||||
allowExclusions={false}
|
allowExclusions={false}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
asFilterButton={true}
|
asFilterButton={true}
|
||||||
|
showLogicalConditionSwitch={showLogicalConditionSwitch}
|
||||||
|
useLogicalAND={showLogicalConditionSwitch && urlParams.useLogicalAndFor?.includes(field)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import { useMemo, useEffect, useCallback, useRef } from 'react';
|
import { useMemo, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { isLogicalAndField } from '../../../../../../../common/constants';
|
||||||
import { MonitorFiltersResult } from '../../../../../../../common/runtime_types';
|
import { MonitorFiltersResult } from '../../../../../../../common/runtime_types';
|
||||||
import {
|
import {
|
||||||
MonitorFilterState,
|
MonitorFilterState,
|
||||||
|
@ -59,6 +60,20 @@ export function useMonitorFiltersState() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { useLogicalAndFor } = urlParams;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(
|
||||||
|
setOverviewPageStateAction({
|
||||||
|
useLogicalAndFor,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
updateManagementPageStateAction({
|
||||||
|
useLogicalAndFor,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, useLogicalAndFor]);
|
||||||
|
|
||||||
const serializeFilterValue = useCallback(
|
const serializeFilterValue = useCallback(
|
||||||
(field: FilterFieldWithQuery, selectedValues: string[] | undefined) => {
|
(field: FilterFieldWithQuery, selectedValues: string[] | undefined) => {
|
||||||
|
@ -92,13 +107,28 @@ export function useMonitorFiltersState() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFilterChange: SyntheticsMonitorFilterChangeHandler = useCallback(
|
const handleFilterChange: SyntheticsMonitorFilterChangeHandler = useCallback(
|
||||||
(field: SyntheticsMonitorFilterField, selectedValues: string[] | undefined) => {
|
(
|
||||||
// Update url to reflect the changed filter
|
field: SyntheticsMonitorFilterField,
|
||||||
updateUrlParams({
|
selectedValues: string[] | undefined,
|
||||||
|
isLogicalAND?: boolean
|
||||||
|
) => {
|
||||||
|
const newUrlParams: Partial<Record<SyntheticsMonitorFilterField, string>> = {
|
||||||
[field]: serializeFilterValue(field, selectedValues),
|
[field]: serializeFilterValue(field, selectedValues),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isLogicalAndField(field)) {
|
||||||
|
const currentUseLogicalAndFor = urlParams.useLogicalAndFor || [];
|
||||||
|
newUrlParams.useLogicalAndFor = serializeFilterValue(
|
||||||
|
'useLogicalAndFor',
|
||||||
|
isLogicalAND
|
||||||
|
? [...currentUseLogicalAndFor, field]
|
||||||
|
: currentUseLogicalAndFor.filter((item: string) => item !== field)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Update url to reflect the changed filter
|
||||||
|
updateUrlParams(newUrlParams);
|
||||||
},
|
},
|
||||||
[serializeFilterValue, updateUrlParams]
|
[serializeFilterValue, updateUrlParams, urlParams.useLogicalAndFor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const reduxState = useSelector(selectMonitorFiltersAndQueryState);
|
const reduxState = useSelector(selectMonitorFiltersAndQueryState);
|
||||||
|
|
|
@ -100,20 +100,17 @@ describe('useMonitorFilters', () => {
|
||||||
it('should handle a combination of parameters', () => {
|
it('should handle a combination of parameters', () => {
|
||||||
spaceSpy.mockReturnValue({ space: { id: 'space3' } } as any);
|
spaceSpy.mockReturnValue({ space: { id: 'space3' } } as any);
|
||||||
paramSpy.mockReturnValue({
|
paramSpy.mockReturnValue({
|
||||||
schedules: 'daily',
|
|
||||||
projects: ['projectA'],
|
projects: ['projectA'],
|
||||||
tags: ['tagB'],
|
tags: ['tagB'],
|
||||||
locations: ['locationC'],
|
locations: ['locationC'],
|
||||||
monitorTypes: 'http',
|
monitorTypes: 'http',
|
||||||
} as any);
|
} as any);
|
||||||
selSPy.mockReturnValue({ status: { allIds: ['id3', 'id4'] } });
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useMonitorFilters({ forAlerts: false }), {
|
const { result } = renderHook(() => useMonitorFilters({ forAlerts: false }), {
|
||||||
wrapper: WrappedHelper,
|
wrapper: WrappedHelper,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current).toEqual([
|
expect(result.current).toEqual([
|
||||||
{ field: 'monitor.id', values: ['id3', 'id4'] },
|
|
||||||
{ field: 'monitor.project.id', values: ['projectA'] },
|
{ field: 'monitor.project.id', values: ['projectA'] },
|
||||||
{ field: 'monitor.type', values: ['http'] },
|
{ field: 'monitor.type', values: ['http'] },
|
||||||
{ field: 'tags', values: ['tagB'] },
|
{ field: 'tags', values: ['tagB'] },
|
||||||
|
|
|
@ -7,23 +7,51 @@
|
||||||
|
|
||||||
import { UrlFilter } from '@kbn/exploratory-view-plugin/public';
|
import { UrlFilter } from '@kbn/exploratory-view-plugin/public';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty, uniqueId } from 'lodash';
|
||||||
import { useGetUrlParams } from '../../../hooks/use_url_params';
|
import { useGetUrlParams } from '../../../hooks/use_url_params';
|
||||||
import { useKibanaSpace } from '../../../../../hooks/use_kibana_space';
|
import { useKibanaSpace } from '../../../../../hooks/use_kibana_space';
|
||||||
import { selectOverviewStatus } from '../../../state/overview_status';
|
import { selectOverviewStatus } from '../../../state/overview_status';
|
||||||
|
|
||||||
|
const createFiltersForField = ({
|
||||||
|
field,
|
||||||
|
values,
|
||||||
|
useLogicalAnd = false,
|
||||||
|
}: {
|
||||||
|
field: string;
|
||||||
|
values: string | string[] | undefined;
|
||||||
|
useLogicalAnd?: boolean;
|
||||||
|
}): UrlFilter[] => {
|
||||||
|
if (!values || !values.length) return [];
|
||||||
|
|
||||||
|
const valueArray = getValues(values);
|
||||||
|
|
||||||
|
return useLogicalAnd
|
||||||
|
? valueArray.map((value) => ({ field, values: [value] }))
|
||||||
|
: [{ field, values: valueArray }];
|
||||||
|
};
|
||||||
|
|
||||||
export const useMonitorFilters = ({ forAlerts }: { forAlerts?: boolean }): UrlFilter[] => {
|
export const useMonitorFilters = ({ forAlerts }: { forAlerts?: boolean }): UrlFilter[] => {
|
||||||
const { space } = useKibanaSpace();
|
const { space } = useKibanaSpace();
|
||||||
const { locations, monitorTypes, tags, projects, schedules } = useGetUrlParams();
|
const { locations, monitorTypes, tags, projects, schedules, useLogicalAndFor } =
|
||||||
|
useGetUrlParams();
|
||||||
const { status: overviewStatus } = useSelector(selectOverviewStatus);
|
const { status: overviewStatus } = useSelector(selectOverviewStatus);
|
||||||
const allIds = overviewStatus?.allIds ?? [];
|
const allIds = overviewStatus?.allIds ?? [];
|
||||||
|
|
||||||
return [
|
|
||||||
// since schedule isn't available in heartbeat data, in that case we rely on monitor.id
|
// since schedule isn't available in heartbeat data, in that case we rely on monitor.id
|
||||||
...(allIds?.length && !isEmpty(schedules) ? [{ field: 'monitor.id', values: allIds }] : []),
|
// We need to rely on monitor.id also for locations, because each heartbeat data only contains one location
|
||||||
|
if (!isEmpty(schedules) || (!isEmpty(locations) && useLogicalAndFor?.includes('locations'))) {
|
||||||
|
// If allIds is empty we return an array with a random id just to not get any result, there's probably a better solution
|
||||||
|
return [{ field: 'monitor.id', values: allIds.length ? allIds : [uniqueId()] }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
...(projects?.length ? [{ field: 'monitor.project.id', values: getValues(projects) }] : []),
|
...(projects?.length ? [{ field: 'monitor.project.id', values: getValues(projects) }] : []),
|
||||||
...(monitorTypes?.length ? [{ field: 'monitor.type', values: getValues(monitorTypes) }] : []),
|
...(monitorTypes?.length ? [{ field: 'monitor.type', values: getValues(monitorTypes) }] : []),
|
||||||
...(tags?.length ? [{ field: 'tags', values: getValues(tags) }] : []),
|
...createFiltersForField({
|
||||||
|
useLogicalAnd: useLogicalAndFor?.includes('tags'),
|
||||||
|
field: 'tags',
|
||||||
|
values: tags,
|
||||||
|
}),
|
||||||
...(locations?.length ? [{ field: 'observer.geo.name', values: getValues(locations) }] : []),
|
...(locations?.length ? [{ field: 'observer.geo.name', values: getValues(locations) }] : []),
|
||||||
...(space
|
...(space
|
||||||
? [{ field: forAlerts ? 'kibana.space_ids' : 'meta.space_id', values: [space.id] }]
|
? [{ field: forAlerts ? 'kibana.space_ids' : 'meta.space_id', values: [space.id] }]
|
||||||
|
|
|
@ -43,7 +43,14 @@ describe('useMonitorList', () => {
|
||||||
handleFilterChange: jest.fn(),
|
handleFilterChange: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
filterState = { locations: [], monitorTypes: [], projects: [], schedules: [], tags: [] };
|
filterState = {
|
||||||
|
locations: [],
|
||||||
|
monitorTypes: [],
|
||||||
|
projects: [],
|
||||||
|
schedules: [],
|
||||||
|
tags: [],
|
||||||
|
useLogicalAndFor: [],
|
||||||
|
};
|
||||||
filterStateWithQuery = { ...filterState, query: 'xyz' };
|
filterStateWithQuery = { ...filterState, query: 'xyz' };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { parse, stringify } from 'query-string';
|
import { parse, stringify } from 'query-string';
|
||||||
import { useLocation, useHistory } from 'react-router-dom';
|
import { useLocation, useHistory } from 'react-router-dom';
|
||||||
import { SyntheticsUrlParams, getSupportedUrlParams } from '../utils/url_params';
|
import { SyntheticsUrlParams, getSupportedUrlParams } from '../utils/url_params';
|
||||||
|
@ -16,7 +16,7 @@ function getParsedParams(search: string) {
|
||||||
|
|
||||||
export type GetUrlParams = () => SyntheticsUrlParams;
|
export type GetUrlParams = () => SyntheticsUrlParams;
|
||||||
export type UpdateUrlParams = (
|
export type UpdateUrlParams = (
|
||||||
updatedParams: Partial<SyntheticsUrlParams> | null,
|
updatedParams: Partial<Record<keyof SyntheticsUrlParams, string>> | null,
|
||||||
replaceState?: boolean
|
replaceState?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
@ -25,7 +25,9 @@ export type SyntheticsUrlParamsHook = () => [GetUrlParams, UpdateUrlParams];
|
||||||
export const useGetUrlParams: GetUrlParams = () => {
|
export const useGetUrlParams: GetUrlParams = () => {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
return getSupportedUrlParams(getParsedParams(search));
|
const urlParams = useMemo(() => getSupportedUrlParams(getParsedParams(search)), [search]);
|
||||||
|
|
||||||
|
return urlParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUrlParams: SyntheticsUrlParamsHook = () => {
|
export const useUrlParams: SyntheticsUrlParamsHook = () => {
|
||||||
|
|
|
@ -37,6 +37,7 @@ function toMonitorManagementListQueryArgs(
|
||||||
searchFields: [],
|
searchFields: [],
|
||||||
internal: true,
|
internal: true,
|
||||||
showFromAllSpaces: pageState.showFromAllSpaces,
|
showFromAllSpaces: pageState.showFromAllSpaces,
|
||||||
|
useLogicalAndFor: pageState.useLogicalAndFor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { ErrorToastOptions } from '@kbn/core-notifications-browser';
|
import { ErrorToastOptions } from '@kbn/core-notifications-browser';
|
||||||
|
|
||||||
|
import { UseLogicalAndField } from '../../../../../common/constants';
|
||||||
import type { MonitorListSortField } from '../../../../../common/runtime_types/monitor_management/sort_field';
|
import type { MonitorListSortField } from '../../../../../common/runtime_types/monitor_management/sort_field';
|
||||||
import {
|
import {
|
||||||
EncryptedSyntheticsMonitor,
|
EncryptedSyntheticsMonitor,
|
||||||
|
@ -25,6 +26,7 @@ export interface MonitorFilterState {
|
||||||
locations?: string[];
|
locations?: string[];
|
||||||
monitorQueryIds?: string[]; // Monitor Query IDs
|
monitorQueryIds?: string[]; // Monitor Query IDs
|
||||||
showFromAllSpaces?: boolean;
|
showFromAllSpaces?: boolean;
|
||||||
|
useLogicalAndFor?: UseLogicalAndField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MonitorListPageState extends MonitorFilterState {
|
export interface MonitorListPageState extends MonitorFilterState {
|
||||||
|
|
|
@ -23,11 +23,17 @@ export const selectEncryptedSyntheticsSavedMonitors = createSelector(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectMonitorFiltersAndQueryState = createSelector(selectMonitorListState, (state) => {
|
export const selectMonitorFiltersAndQueryState = createSelector(selectMonitorListState, (state) => {
|
||||||
const { monitorTypes, tags, locations, projects, schedules }: MonitorFilterState =
|
const {
|
||||||
state.pageState;
|
monitorTypes,
|
||||||
|
tags,
|
||||||
|
locations,
|
||||||
|
projects,
|
||||||
|
schedules,
|
||||||
|
useLogicalAndFor,
|
||||||
|
}: MonitorFilterState = state.pageState;
|
||||||
const { query } = state.pageState;
|
const { query } = state.pageState;
|
||||||
|
|
||||||
return { monitorTypes, tags, locations, projects, schedules, query };
|
return { monitorTypes, tags, locations, projects, schedules, query, useLogicalAndFor };
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectMonitorUpsertStatuses = (state: SyntheticsAppState) =>
|
export const selectMonitorUpsertStatuses = (state: SyntheticsAppState) =>
|
||||||
|
|
|
@ -27,6 +27,7 @@ export function toStatusOverviewQueryArgs(
|
||||||
monitorQueryIds: pageState.monitorQueryIds,
|
monitorQueryIds: pageState.monitorQueryIds,
|
||||||
showFromAllSpaces: pageState.showFromAllSpaces,
|
showFromAllSpaces: pageState.showFromAllSpaces,
|
||||||
searchFields: [],
|
searchFields: [],
|
||||||
|
useLogicalAndFor: pageState.useLogicalAndFor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,12 +27,13 @@ export interface SyntheticsMonitorFilterItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMonitorFilterFields(): SyntheticsMonitorFilterField[] {
|
export function getMonitorFilterFields(): SyntheticsMonitorFilterField[] {
|
||||||
return ['tags', 'locations', 'monitorTypes', 'projects', 'schedules'];
|
return ['tags', 'locations', 'monitorTypes', 'projects', 'schedules', 'useLogicalAndFor'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyntheticsMonitorFilterChangeHandler = (
|
export type SyntheticsMonitorFilterChangeHandler = (
|
||||||
field: SyntheticsMonitorFilterField,
|
field: SyntheticsMonitorFilterField,
|
||||||
selectedValues: string[] | undefined
|
selectedValues: string[] | undefined,
|
||||||
|
isLogicalAND?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export function getSyntheticsFilterDisplayValues(
|
export function getSyntheticsFilterDisplayValues(
|
||||||
|
|
|
@ -70,6 +70,7 @@ describe('getSupportedUrlParams', () => {
|
||||||
projects: [],
|
projects: [],
|
||||||
schedules: [],
|
schedules: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
useLogicalAndFor: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import { MonitorOverviewState } from '../../state';
|
import { MonitorOverviewState } from '../../state';
|
||||||
import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../common/constants/synthetics/client_defaults';
|
import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../common/constants/synthetics/client_defaults';
|
||||||
import { CLIENT_DEFAULTS } from '../../../../../common/constants';
|
import { CLIENT_DEFAULTS, UseLogicalAndField } from '../../../../../common/constants';
|
||||||
import { parseAbsoluteDate } from './parse_absolute_date';
|
import { parseAbsoluteDate } from './parse_absolute_date';
|
||||||
|
|
||||||
// TODO: Change for Synthetics App if needed (Copied from legacy_uptime)
|
// TODO: Change for Synthetics App if needed (Copied from legacy_uptime)
|
||||||
|
@ -35,6 +35,7 @@ export interface SyntheticsUrlParams {
|
||||||
packagePolicyId?: string;
|
packagePolicyId?: string;
|
||||||
cloneId?: string;
|
cloneId?: string;
|
||||||
spaceId?: string;
|
spaceId?: string;
|
||||||
|
useLogicalAndFor?: UseLogicalAndField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ABSOLUTE_DATE_RANGE_START, ABSOLUTE_DATE_RANGE_END, SEARCH, FILTERS, STATUS_FILTER } =
|
const { ABSOLUTE_DATE_RANGE_START, ABSOLUTE_DATE_RANGE_END, SEARCH, FILTERS, STATUS_FILTER } =
|
||||||
|
@ -91,6 +92,7 @@ export const getSupportedUrlParams = (params: {
|
||||||
groupOrderBy,
|
groupOrderBy,
|
||||||
packagePolicyId,
|
packagePolicyId,
|
||||||
spaceId,
|
spaceId,
|
||||||
|
useLogicalAndFor,
|
||||||
} = filteredParams;
|
} = filteredParams;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -123,6 +125,7 @@ export const getSupportedUrlParams = (params: {
|
||||||
locationId: locationId || undefined,
|
locationId: locationId || undefined,
|
||||||
cloneId: filteredParams.cloneId,
|
cloneId: filteredParams.cloneId,
|
||||||
spaceId: spaceId || undefined,
|
spaceId: spaceId || undefined,
|
||||||
|
useLogicalAndFor: parseFilters(useLogicalAndFor),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { schema, TypeOf } from '@kbn/config-schema';
|
import { schema, Type, TypeOf } from '@kbn/config-schema';
|
||||||
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { escapeQuotes } from '@kbn/es-query';
|
import { escapeQuotes } from '@kbn/es-query';
|
||||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||||
|
import { useLogicalAndFields } from '../../common/constants';
|
||||||
import { RouteContext } from './types';
|
import { RouteContext } from './types';
|
||||||
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
|
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
|
||||||
import { getAllLocations } from '../synthetics_service/get_all_locations';
|
import { getAllLocations } from '../synthetics_service/get_all_locations';
|
||||||
|
@ -21,11 +22,11 @@ const StringOrArraySchema = schema.maybe(
|
||||||
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
|
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
|
||||||
);
|
);
|
||||||
|
|
||||||
export const QuerySchema = schema.object({
|
const UseLogicalAndFieldLiterals = useLogicalAndFields.map((f) => schema.literal(f)) as [
|
||||||
page: schema.maybe(schema.number()),
|
Type<string>
|
||||||
perPage: schema.maybe(schema.number()),
|
];
|
||||||
sortField: MonitorSortFieldSchema,
|
|
||||||
sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])),
|
const CommonQuerySchema = {
|
||||||
query: schema.maybe(schema.string()),
|
query: schema.maybe(schema.string()),
|
||||||
filter: schema.maybe(schema.string()),
|
filter: schema.maybe(schema.string()),
|
||||||
tags: StringOrArraySchema,
|
tags: StringOrArraySchema,
|
||||||
|
@ -34,30 +35,32 @@ export const QuerySchema = schema.object({
|
||||||
projects: StringOrArraySchema,
|
projects: StringOrArraySchema,
|
||||||
schedules: StringOrArraySchema,
|
schedules: StringOrArraySchema,
|
||||||
status: StringOrArraySchema,
|
status: StringOrArraySchema,
|
||||||
searchAfter: schema.maybe(schema.arrayOf(schema.string())),
|
|
||||||
monitorQueryIds: StringOrArraySchema,
|
monitorQueryIds: StringOrArraySchema,
|
||||||
|
showFromAllSpaces: schema.maybe(schema.boolean()),
|
||||||
|
useLogicalAndFor: schema.maybe(
|
||||||
|
schema.oneOf([schema.string(), schema.arrayOf(schema.oneOf(UseLogicalAndFieldLiterals))])
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuerySchema = schema.object({
|
||||||
|
...CommonQuerySchema,
|
||||||
|
page: schema.maybe(schema.number()),
|
||||||
|
perPage: schema.maybe(schema.number()),
|
||||||
|
sortField: MonitorSortFieldSchema,
|
||||||
|
sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])),
|
||||||
|
searchAfter: schema.maybe(schema.arrayOf(schema.string())),
|
||||||
internal: schema.maybe(
|
internal: schema.maybe(
|
||||||
schema.boolean({
|
schema.boolean({
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
showFromAllSpaces: schema.maybe(schema.boolean()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MonitorsQuery = TypeOf<typeof QuerySchema>;
|
export type MonitorsQuery = TypeOf<typeof QuerySchema>;
|
||||||
|
|
||||||
export const OverviewStatusSchema = schema.object({
|
export const OverviewStatusSchema = schema.object({
|
||||||
query: schema.maybe(schema.string()),
|
...CommonQuerySchema,
|
||||||
filter: schema.maybe(schema.string()),
|
|
||||||
tags: StringOrArraySchema,
|
|
||||||
monitorTypes: StringOrArraySchema,
|
|
||||||
locations: StringOrArraySchema,
|
|
||||||
projects: StringOrArraySchema,
|
|
||||||
monitorQueryIds: StringOrArraySchema,
|
|
||||||
schedules: StringOrArraySchema,
|
|
||||||
status: StringOrArraySchema,
|
|
||||||
scopeStatusByLocation: schema.maybe(schema.boolean()),
|
scopeStatusByLocation: schema.maybe(schema.boolean()),
|
||||||
showFromAllSpaces: schema.maybe(schema.boolean()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type OverviewStatusQuery = TypeOf<typeof OverviewStatusSchema>;
|
export type OverviewStatusQuery = TypeOf<typeof OverviewStatusSchema>;
|
||||||
|
@ -113,7 +116,9 @@ interface Filters {
|
||||||
configIds?: string | string[];
|
configIds?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMonitorFilters = async (context: RouteContext) => {
|
export const getMonitorFilters = async (
|
||||||
|
context: RouteContext<Record<string, any>, OverviewStatusQuery>
|
||||||
|
) => {
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
monitorTypes,
|
monitorTypes,
|
||||||
|
@ -122,10 +127,12 @@ export const getMonitorFilters = async (context: RouteContext) => {
|
||||||
schedules,
|
schedules,
|
||||||
monitorQueryIds,
|
monitorQueryIds,
|
||||||
locations: queryLocations,
|
locations: queryLocations,
|
||||||
|
useLogicalAndFor,
|
||||||
} = context.request.query;
|
} = context.request.query;
|
||||||
const locations = await parseLocationFilter(context, queryLocations);
|
const locations = await parseLocationFilter(context, queryLocations);
|
||||||
|
|
||||||
return parseArrayFilters({
|
return parseArrayFilters(
|
||||||
|
{
|
||||||
filter,
|
filter,
|
||||||
tags,
|
tags,
|
||||||
monitorTypes,
|
monitorTypes,
|
||||||
|
@ -133,10 +140,13 @@ export const getMonitorFilters = async (context: RouteContext) => {
|
||||||
schedules,
|
schedules,
|
||||||
monitorQueryIds,
|
monitorQueryIds,
|
||||||
locations,
|
locations,
|
||||||
});
|
},
|
||||||
|
useLogicalAndFor
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseArrayFilters = ({
|
export const parseArrayFilters = (
|
||||||
|
{
|
||||||
tags,
|
tags,
|
||||||
filter,
|
filter,
|
||||||
configIds,
|
configIds,
|
||||||
|
@ -145,13 +155,23 @@ export const parseArrayFilters = ({
|
||||||
schedules,
|
schedules,
|
||||||
monitorQueryIds,
|
monitorQueryIds,
|
||||||
locations,
|
locations,
|
||||||
}: Filters) => {
|
}: Filters,
|
||||||
|
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = []
|
||||||
|
) => {
|
||||||
const filtersStr = [
|
const filtersStr = [
|
||||||
filter,
|
filter,
|
||||||
getSavedObjectKqlFilter({ field: 'tags', values: tags }),
|
getSavedObjectKqlFilter({
|
||||||
|
field: 'tags',
|
||||||
|
values: tags,
|
||||||
|
operator: useLogicalAndFor.includes('tags') ? 'AND' : 'OR',
|
||||||
|
}),
|
||||||
getSavedObjectKqlFilter({ field: 'project_id', values: projects }),
|
getSavedObjectKqlFilter({ field: 'project_id', values: projects }),
|
||||||
getSavedObjectKqlFilter({ field: 'type', values: monitorTypes }),
|
getSavedObjectKqlFilter({ field: 'type', values: monitorTypes }),
|
||||||
getSavedObjectKqlFilter({ field: 'locations.id', values: locations }),
|
getSavedObjectKqlFilter({
|
||||||
|
field: 'locations.id',
|
||||||
|
values: locations,
|
||||||
|
operator: useLogicalAndFor.includes('locations') ? 'AND' : 'OR',
|
||||||
|
}),
|
||||||
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }),
|
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }),
|
||||||
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }),
|
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }),
|
||||||
getSavedObjectKqlFilter({ field: 'config_id', values: configIds }),
|
getSavedObjectKqlFilter({ field: 'config_id', values: configIds }),
|
||||||
|
|
|
@ -25,5 +25,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||||
loadTestFile(require.resolve('./sync_global_params'));
|
loadTestFile(require.resolve('./sync_global_params'));
|
||||||
loadTestFile(require.resolve('./add_edit_params'));
|
loadTestFile(require.resolve('./add_edit_params'));
|
||||||
loadTestFile(require.resolve('./private_location_apis'));
|
loadTestFile(require.resolve('./private_location_apis'));
|
||||||
|
loadTestFile(require.resolve('./list_monitors'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
109
x-pack/test/api_integration/apis/synthetics/list_monitors.ts
Normal file
109
x-pack/test/api_integration/apis/synthetics/list_monitors.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
import expect from '@kbn/expect';
|
||||||
|
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
|
||||||
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
|
||||||
|
export default function ({ getService }: FtrProviderContext) {
|
||||||
|
describe('ListMonitorsAPI', function () {
|
||||||
|
const supertestAPI = getService('supertest');
|
||||||
|
const kibanaServer = getService('kibanaServer');
|
||||||
|
|
||||||
|
const common = {
|
||||||
|
type: 'http',
|
||||||
|
url: 'https://www.elastic.co',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FIRST_TAG = 'a';
|
||||||
|
const SECOND_TAG = 'b';
|
||||||
|
|
||||||
|
const FIRST_LOCATION = 'dev';
|
||||||
|
const SECOND_LOCATION = 'dev2';
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await kibanaServer.savedObjects.cleanStandardList();
|
||||||
|
|
||||||
|
// Create test monitors with different tags
|
||||||
|
const monitorA = {
|
||||||
|
...common,
|
||||||
|
name: 'Monitor A',
|
||||||
|
tags: [FIRST_TAG, SECOND_TAG],
|
||||||
|
locations: [FIRST_LOCATION, SECOND_LOCATION],
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitorB = {
|
||||||
|
...common,
|
||||||
|
name: 'Monitor B',
|
||||||
|
url: 'https://www.elastic.co',
|
||||||
|
tags: [SECOND_TAG],
|
||||||
|
locations: [FIRST_LOCATION],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the test monitors
|
||||||
|
await supertestAPI
|
||||||
|
.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
|
||||||
|
.set('kbn-xsrf', 'true')
|
||||||
|
.send(monitorA);
|
||||||
|
|
||||||
|
await supertestAPI
|
||||||
|
.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
|
||||||
|
.set('kbn-xsrf', 'true')
|
||||||
|
.send(monitorB);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await kibanaServer.savedObjects.cleanStandardList();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useLogicalAndFor parameter', () => {
|
||||||
|
it('should return 2 monitors when not using the useLogicalAndFor query parameter and searching for both tags', async () => {
|
||||||
|
const response = await supertestAPI
|
||||||
|
.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
|
||||||
|
.query({ tags: [FIRST_TAG, SECOND_TAG] })
|
||||||
|
.set('kbn-xsrf', 'true');
|
||||||
|
|
||||||
|
expect(response.status).to.be(200);
|
||||||
|
expect(response.body.monitors.length).to.be(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return only 1 monitor when useLogicalAndFor includes tags and searching for both tags', async () => {
|
||||||
|
const response = await supertestAPI
|
||||||
|
.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
|
||||||
|
.query({
|
||||||
|
tags: [FIRST_TAG, SECOND_TAG],
|
||||||
|
useLogicalAndFor: ['tags'],
|
||||||
|
})
|
||||||
|
.set('kbn-xsrf', 'true');
|
||||||
|
|
||||||
|
expect(response.status).to.be(200);
|
||||||
|
expect(response.body.monitors.length).to.be(1);
|
||||||
|
});
|
||||||
|
it('should return 2 monitors when not using the useLogicalAndFor query parameter and searching for both locations', async () => {
|
||||||
|
const response = await supertestAPI
|
||||||
|
.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
|
||||||
|
.query({ locations: [FIRST_LOCATION, SECOND_LOCATION] })
|
||||||
|
.set('kbn-xsrf', 'true');
|
||||||
|
|
||||||
|
expect(response.status).to.be(200);
|
||||||
|
expect(response.body.monitors.length).to.be(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return only 1 monitor when useLogicalAndFor includes tags and searching for both locations', async () => {
|
||||||
|
const response = await supertestAPI
|
||||||
|
.get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
|
||||||
|
.query({
|
||||||
|
locations: [FIRST_LOCATION, SECOND_LOCATION],
|
||||||
|
useLogicalAndFor: ['locations'],
|
||||||
|
})
|
||||||
|
.set('kbn-xsrf', 'true');
|
||||||
|
|
||||||
|
expect(response.status).to.be(200);
|
||||||
|
expect(response.body.monitors.length).to.be(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue