[Synthetics] Add logical AND to monitor tags and locations filter (#217985)

This commit is contained in:
Francesco Fagnani 2025-04-23 08:01:40 +02:00 committed by GitHub
parent 7ac6488a0e
commit 061b93093e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 415 additions and 78 deletions

View file

@ -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);
};

View file

@ -11,3 +11,4 @@ export * from './capabilities';
export * from './settings_defaults';
export * from './ui';
export * from './synthetics';
export * from './filters_fields_with_logical_and';

View file

@ -6,12 +6,16 @@
*/
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({
page: t.number,
perPage: t.number,
sortField: t.string,
sortOrder: t.union([t.literal('desc'), t.literal('asc')]),
const useLogicalAndFileLiteral = useLogicalAndFields.map((f) => t.literal(f)) as unknown as [
Mixed,
Mixed,
...Mixed[]
];
const FetchMonitorQueryArgsCommon = {
query: t.string,
searchFields: t.array(t.string),
tags: t.array(t.string),
@ -20,8 +24,17 @@ export const FetchMonitorManagementListQueryArgsCodec = t.partial({
projects: t.array(t.string),
schedules: 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,
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<
@ -29,17 +42,7 @@ export type FetchMonitorManagementListQueryArgs = t.TypeOf<
>;
export const FetchMonitorOverviewQueryArgsCodec = t.partial({
query: t.string,
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,
...FetchMonitorQueryArgsCommon,
});
export type FetchMonitorOverviewQueryArgs = t.TypeOf<typeof FetchMonitorOverviewQueryArgsCodec>;

View file

@ -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();
});
});
});

View file

@ -27,3 +27,4 @@ export * from './test_run_details.journey';
export * from './step_details.journey';
export * from './project_monitor_read_only.journey';
export * from './overview_save_lens_visualization.journey';
export * from './filter_monitors.journey';

View file

@ -7,6 +7,7 @@
import React, { useState } from 'react';
import { FieldValueSelection } from '@kbn/observability-shared-plugin/public';
import { isLogicalAndField } from '../../../../../../../common/constants';
import {
getSyntheticsFilterDisplayValues,
SyntheticsMonitorFilterItem,
@ -37,6 +38,8 @@ export const FilterButton = ({
[]
).map(({ label: selectedValueLabel }) => selectedValueLabel);
const showLogicalConditionSwitch = isLogicalAndField(field);
return (
<FieldValueSelection
selectedValue={selectedValueLabels}
@ -48,10 +51,14 @@ export const FilterButton = ({
: values
}
setQuery={setQuery}
onChange={(selectedValues) => handleFilterChange(field, selectedValues)}
onChange={(selectedValues, _, isLogicalAND) =>
handleFilterChange(field, selectedValues, isLogicalAND)
}
allowExclusions={false}
loading={loading}
asFilterButton={true}
showLogicalConditionSwitch={showLogicalConditionSwitch}
useLogicalAND={showLogicalConditionSwitch && urlParams.useLogicalAndFor?.includes(field)}
/>
);
};

View file

@ -8,6 +8,7 @@
import { useMemo, useEffect, useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { isLogicalAndField } from '../../../../../../../common/constants';
import { MonitorFiltersResult } from '../../../../../../../common/runtime_types';
import {
MonitorFilterState,
@ -59,6 +60,20 @@ export function useMonitorFiltersState() {
}, []);
const dispatch = useDispatch();
const { useLogicalAndFor } = urlParams;
useEffect(() => {
dispatch(
setOverviewPageStateAction({
useLogicalAndFor,
})
);
dispatch(
updateManagementPageStateAction({
useLogicalAndFor,
})
);
}, [dispatch, useLogicalAndFor]);
const serializeFilterValue = useCallback(
(field: FilterFieldWithQuery, selectedValues: string[] | undefined) => {
@ -92,13 +107,28 @@ export function useMonitorFiltersState() {
);
const handleFilterChange: SyntheticsMonitorFilterChangeHandler = useCallback(
(field: SyntheticsMonitorFilterField, selectedValues: string[] | undefined) => {
// Update url to reflect the changed filter
updateUrlParams({
(
field: SyntheticsMonitorFilterField,
selectedValues: string[] | undefined,
isLogicalAND?: boolean
) => {
const newUrlParams: Partial<Record<SyntheticsMonitorFilterField, string>> = {
[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);

View file

@ -100,20 +100,17 @@ describe('useMonitorFilters', () => {
it('should handle a combination of parameters', () => {
spaceSpy.mockReturnValue({ space: { id: 'space3' } } as any);
paramSpy.mockReturnValue({
schedules: 'daily',
projects: ['projectA'],
tags: ['tagB'],
locations: ['locationC'],
monitorTypes: 'http',
} as any);
selSPy.mockReturnValue({ status: { allIds: ['id3', 'id4'] } });
const { result } = renderHook(() => useMonitorFilters({ forAlerts: false }), {
wrapper: WrappedHelper,
});
expect(result.current).toEqual([
{ field: 'monitor.id', values: ['id3', 'id4'] },
{ field: 'monitor.project.id', values: ['projectA'] },
{ field: 'monitor.type', values: ['http'] },
{ field: 'tags', values: ['tagB'] },

View file

@ -7,23 +7,51 @@
import { UrlFilter } from '@kbn/exploratory-view-plugin/public';
import { useSelector } from 'react-redux';
import { isEmpty } from 'lodash';
import { isEmpty, uniqueId } from 'lodash';
import { useGetUrlParams } from '../../../hooks/use_url_params';
import { useKibanaSpace } from '../../../../../hooks/use_kibana_space';
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[] => {
const { space } = useKibanaSpace();
const { locations, monitorTypes, tags, projects, schedules } = useGetUrlParams();
const { locations, monitorTypes, tags, projects, schedules, useLogicalAndFor } =
useGetUrlParams();
const { status: overviewStatus } = useSelector(selectOverviewStatus);
const allIds = overviewStatus?.allIds ?? [];
// since schedule isn't available in heartbeat data, in that case we rely on monitor.id
// 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 [
// 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 }] : []),
...(projects?.length ? [{ field: 'monitor.project.id', values: getValues(projects) }] : []),
...(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) }] : []),
...(space
? [{ field: forAlerts ? 'kibana.space_ids' : 'meta.space_id', values: [space.id] }]

View file

@ -43,7 +43,14 @@ describe('useMonitorList', () => {
handleFilterChange: jest.fn(),
};
filterState = { locations: [], monitorTypes: [], projects: [], schedules: [], tags: [] };
filterState = {
locations: [],
monitorTypes: [],
projects: [],
schedules: [],
tags: [],
useLogicalAndFor: [],
};
filterStateWithQuery = { ...filterState, query: 'xyz' };
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { parse, stringify } from 'query-string';
import { useLocation, useHistory } from 'react-router-dom';
import { SyntheticsUrlParams, getSupportedUrlParams } from '../utils/url_params';
@ -16,7 +16,7 @@ function getParsedParams(search: string) {
export type GetUrlParams = () => SyntheticsUrlParams;
export type UpdateUrlParams = (
updatedParams: Partial<SyntheticsUrlParams> | null,
updatedParams: Partial<Record<keyof SyntheticsUrlParams, string>> | null,
replaceState?: boolean
) => void;
@ -25,7 +25,9 @@ export type SyntheticsUrlParamsHook = () => [GetUrlParams, UpdateUrlParams];
export const useGetUrlParams: GetUrlParams = () => {
const { search } = useLocation();
return getSupportedUrlParams(getParsedParams(search));
const urlParams = useMemo(() => getSupportedUrlParams(getParsedParams(search)), [search]);
return urlParams;
};
export const useUrlParams: SyntheticsUrlParamsHook = () => {

View file

@ -37,6 +37,7 @@ function toMonitorManagementListQueryArgs(
searchFields: [],
internal: true,
showFromAllSpaces: pageState.showFromAllSpaces,
useLogicalAndFor: pageState.useLogicalAndFor,
};
}

View file

@ -7,6 +7,7 @@
import { ErrorToastOptions } from '@kbn/core-notifications-browser';
import { UseLogicalAndField } from '../../../../../common/constants';
import type { MonitorListSortField } from '../../../../../common/runtime_types/monitor_management/sort_field';
import {
EncryptedSyntheticsMonitor,
@ -25,6 +26,7 @@ export interface MonitorFilterState {
locations?: string[];
monitorQueryIds?: string[]; // Monitor Query IDs
showFromAllSpaces?: boolean;
useLogicalAndFor?: UseLogicalAndField[];
}
export interface MonitorListPageState extends MonitorFilterState {

View file

@ -23,11 +23,17 @@ export const selectEncryptedSyntheticsSavedMonitors = createSelector(
);
export const selectMonitorFiltersAndQueryState = createSelector(selectMonitorListState, (state) => {
const { monitorTypes, tags, locations, projects, schedules }: MonitorFilterState =
state.pageState;
const {
monitorTypes,
tags,
locations,
projects,
schedules,
useLogicalAndFor,
}: MonitorFilterState = 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) =>

View file

@ -27,6 +27,7 @@ export function toStatusOverviewQueryArgs(
monitorQueryIds: pageState.monitorQueryIds,
showFromAllSpaces: pageState.showFromAllSpaces,
searchFields: [],
useLogicalAndFor: pageState.useLogicalAndFor,
};
}

View file

@ -27,12 +27,13 @@ export interface SyntheticsMonitorFilterItem {
}
export function getMonitorFilterFields(): SyntheticsMonitorFilterField[] {
return ['tags', 'locations', 'monitorTypes', 'projects', 'schedules'];
return ['tags', 'locations', 'monitorTypes', 'projects', 'schedules', 'useLogicalAndFor'];
}
export type SyntheticsMonitorFilterChangeHandler = (
field: SyntheticsMonitorFilterField,
selectedValues: string[] | undefined
selectedValues: string[] | undefined,
isLogicalAND?: boolean
) => void;
export function getSyntheticsFilterDisplayValues(

View file

@ -70,6 +70,7 @@ describe('getSupportedUrlParams', () => {
projects: [],
schedules: [],
tags: [],
useLogicalAndFor: [],
});
});

View file

@ -7,7 +7,7 @@
import { MonitorOverviewState } from '../../state';
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';
// TODO: Change for Synthetics App if needed (Copied from legacy_uptime)
@ -35,6 +35,7 @@ export interface SyntheticsUrlParams {
packagePolicyId?: string;
cloneId?: string;
spaceId?: string;
useLogicalAndFor?: UseLogicalAndField[];
}
const { ABSOLUTE_DATE_RANGE_START, ABSOLUTE_DATE_RANGE_END, SEARCH, FILTERS, STATUS_FILTER } =
@ -91,6 +92,7 @@ export const getSupportedUrlParams = (params: {
groupOrderBy,
packagePolicyId,
spaceId,
useLogicalAndFor,
} = filteredParams;
return {
@ -123,6 +125,7 @@ export const getSupportedUrlParams = (params: {
locationId: locationId || undefined,
cloneId: filteredParams.cloneId,
spaceId: spaceId || undefined,
useLogicalAndFor: parseFilters(useLogicalAndFor),
};
};

View file

@ -5,11 +5,12 @@
* 2.0.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { schema, Type, TypeOf } from '@kbn/config-schema';
import { SavedObjectsFindResponse } from '@kbn/core/server';
import { isEmpty } from 'lodash';
import { escapeQuotes } from '@kbn/es-query';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { useLogicalAndFields } from '../../common/constants';
import { RouteContext } from './types';
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
import { getAllLocations } from '../synthetics_service/get_all_locations';
@ -21,11 +22,11 @@ const StringOrArraySchema = schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
);
export const QuerySchema = schema.object({
page: schema.maybe(schema.number()),
perPage: schema.maybe(schema.number()),
sortField: MonitorSortFieldSchema,
sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])),
const UseLogicalAndFieldLiterals = useLogicalAndFields.map((f) => schema.literal(f)) as [
Type<string>
];
const CommonQuerySchema = {
query: schema.maybe(schema.string()),
filter: schema.maybe(schema.string()),
tags: StringOrArraySchema,
@ -34,30 +35,32 @@ export const QuerySchema = schema.object({
projects: StringOrArraySchema,
schedules: StringOrArraySchema,
status: StringOrArraySchema,
searchAfter: schema.maybe(schema.arrayOf(schema.string())),
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(
schema.boolean({
defaultValue: false,
})
),
showFromAllSpaces: schema.maybe(schema.boolean()),
});
export type MonitorsQuery = TypeOf<typeof QuerySchema>;
export const OverviewStatusSchema = schema.object({
query: schema.maybe(schema.string()),
filter: schema.maybe(schema.string()),
tags: StringOrArraySchema,
monitorTypes: StringOrArraySchema,
locations: StringOrArraySchema,
projects: StringOrArraySchema,
monitorQueryIds: StringOrArraySchema,
schedules: StringOrArraySchema,
status: StringOrArraySchema,
...CommonQuerySchema,
scopeStatusByLocation: schema.maybe(schema.boolean()),
showFromAllSpaces: schema.maybe(schema.boolean()),
});
export type OverviewStatusQuery = TypeOf<typeof OverviewStatusSchema>;
@ -113,7 +116,9 @@ interface Filters {
configIds?: string | string[];
}
export const getMonitorFilters = async (context: RouteContext) => {
export const getMonitorFilters = async (
context: RouteContext<Record<string, any>, OverviewStatusQuery>
) => {
const {
tags,
monitorTypes,
@ -122,36 +127,51 @@ export const getMonitorFilters = async (context: RouteContext) => {
schedules,
monitorQueryIds,
locations: queryLocations,
useLogicalAndFor,
} = context.request.query;
const locations = await parseLocationFilter(context, queryLocations);
return parseArrayFilters({
filter,
return parseArrayFilters(
{
filter,
tags,
monitorTypes,
projects,
schedules,
monitorQueryIds,
locations,
},
useLogicalAndFor
);
};
export const parseArrayFilters = (
{
tags,
monitorTypes,
filter,
configIds,
projects,
monitorTypes,
schedules,
monitorQueryIds,
locations,
});
};
export const parseArrayFilters = ({
tags,
filter,
configIds,
projects,
monitorTypes,
schedules,
monitorQueryIds,
locations,
}: Filters) => {
}: Filters,
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = []
) => {
const filtersStr = [
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: '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: 'id', values: monitorQueryIds }),
getSavedObjectKqlFilter({ field: 'config_id', values: configIds }),

View file

@ -25,5 +25,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./sync_global_params'));
loadTestFile(require.resolve('./add_edit_params'));
loadTestFile(require.resolve('./private_location_apis'));
loadTestFile(require.resolve('./list_monitors'));
});
}

View 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);
});
});
});
}