mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -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 './ui';
|
||||
export * from './synthetics';
|
||||
export * from './filters_fields_with_logical_and';
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 './project_monitor_read_only.journey';
|
||||
export * from './overview_save_lens_visualization.journey';
|
||||
export * from './filter_monitors.journey';
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'] },
|
||||
|
|
|
@ -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] }]
|
||||
|
|
|
@ -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' };
|
||||
});
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -37,6 +37,7 @@ function toMonitorManagementListQueryArgs(
|
|||
searchFields: [],
|
||||
internal: true,
|
||||
showFromAllSpaces: pageState.showFromAllSpaces,
|
||||
useLogicalAndFor: pageState.useLogicalAndFor,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -27,6 +27,7 @@ export function toStatusOverviewQueryArgs(
|
|||
monitorQueryIds: pageState.monitorQueryIds,
|
||||
showFromAllSpaces: pageState.showFromAllSpaces,
|
||||
searchFields: [],
|
||||
useLogicalAndFor: pageState.useLogicalAndFor,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -70,6 +70,7 @@ describe('getSupportedUrlParams', () => {
|
|||
projects: [],
|
||||
schedules: [],
|
||||
tags: [],
|
||||
useLogicalAndFor: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
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