diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index dedc390c4771..c01295f6ee42 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -515,7 +515,6 @@ export const useField = ( if (resetValue) { hasBeenReset.current = true; const newValue = deserializeValue(updatedDefaultValue ?? defaultValue); - // updateStateIfMounted('value', newValue); setValue(newValue); return newValue; } diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx b/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx index 01c715583274..8176d3fcbbca 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { of } from 'rxjs'; import { ComponentType } from 'enzyme'; import { LocationDescriptorObject } from 'history'; + import { docLinksServiceMock, uiSettingsServiceMock, @@ -17,6 +18,7 @@ import { scopedHistoryMock, } from '../../../../../../src/core/public/mocks'; import { AppContextProvider } from '../../../public/application/app_context'; +import { AppDeps } from '../../../public/application/app'; import { LicenseStatus } from '../../../common/types/license_status'; class MockTimeBuckets { @@ -35,7 +37,7 @@ history.createHref.mockImplementation((location: LocationDescriptorObject) => { return `${location.pathname}${location.search ? '?' + location.search : ''}`; }); -export const mockContextValue = { +export const mockContextValue: AppDeps = { licenseStatus$: of({ valid: true }), docLinks: docLinksServiceMock.createStartContract(), setBreadcrumbs: jest.fn(), diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts index 961e2a458dc0..09a841ff147a 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/index.ts @@ -11,7 +11,7 @@ import { setup as watchCreateJsonSetup } from './watch_create_json.helpers'; import { setup as watchCreateThresholdSetup } from './watch_create_threshold.helpers'; import { setup as watchEditSetup } from './watch_edit.helpers'; -export { nextTick, getRandomString, findTestSubject, TestBed } from '@kbn/test/jest'; +export { getRandomString, findTestSubject, TestBed } from '@kbn/test/jest'; export { wrapBodyResponse, unwrapBodyResponse } from './body_response'; export { setupEnvironment } from './setup_environment'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts index 05b325ee946b..5ba0387d21ba 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts @@ -7,6 +7,7 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; + import { init as initHttpRequests } from './http_requests'; import { setHttpClient, setSavedObjectsClient } from '../../../public/application/lib/api'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts index c70684b80a6d..caddf1df93d4 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts @@ -93,7 +93,7 @@ export type TestSubjects = | 'toEmailAddressInput' | 'triggerIntervalSizeInput' | 'watchActionAccordion' - | 'watchActionAccordion.mockComboBox' + | 'watchActionAccordion.toEmailAddressInput' | 'watchActionsPanel' | 'watchThresholdButton' | 'watchThresholdInput' diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts index ad171f9e40ca..c0643e70dded 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; -import { registerTestBed, findTestSubject, TestBed, TestBedConfig, nextTick } from '@kbn/test/jest'; +import { registerTestBed, findTestSubject, TestBed, TestBedConfig } from '@kbn/test/jest'; import { WatchList } from '../../../public/application/sections/watch_list/components/watch_list'; import { ROUTES, REFRESH_INTERVALS } from '../../../common/constants'; import { withAppContext } from './app_context.mock'; @@ -24,7 +24,6 @@ const initTestBed = registerTestBed(withAppContext(WatchList), testBedConfig); export interface WatchListTestBed extends TestBed { actions: { selectWatchAt: (index: number) => void; - clickWatchAt: (index: number) => void; clickWatchActionAt: (index: number, action: 'delete' | 'edit') => void; searchWatches: (term: string) => void; advanceTimeToTableRefresh: () => Promise; @@ -45,18 +44,6 @@ export const setup = async (): Promise => { checkBox.simulate('change', { target: { checked: true } }); }; - const clickWatchAt = async (index: number) => { - const { rows } = testBed.table.getMetaData('watchesTable'); - const watchesLink = findTestSubject(rows[index].reactWrapper, 'watchesLink'); - - await act(async () => { - const { href } = watchesLink.props(); - testBed.router.navigateTo(href!); - await nextTick(); - testBed.component.update(); - }); - }; - const clickWatchActionAt = async (index: number, action: 'delete' | 'edit') => { const { component, table } = testBed; const { rows } = table.getMetaData('watchesTable'); @@ -95,7 +82,6 @@ export const setup = async (): Promise => { ...testBed, actions: { selectWatchAt, - clickWatchAt, clickWatchActionAt, searchWatches, advanceTimeToTableRefresh, diff --git a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts index a1c7e8b40499..02b6908fc1d4 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; -import { registerTestBed, findTestSubject, TestBed, TestBedConfig, delay } from '@kbn/test/jest'; +import { registerTestBed, findTestSubject, TestBed, TestBedConfig } from '@kbn/test/jest'; import { WatchStatus } from '../../../public/application/sections/watch_status/components/watch_status'; import { ROUTES } from '../../../common/constants'; import { WATCH_ID } from './jest_constants'; @@ -89,9 +89,8 @@ export const setup = async (): Promise => { await act(async () => { button.simulate('click'); - await delay(100); - component.update(); }); + component.update(); }; return { diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index 4a632d9752ca..f9ea51a80ae7 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { getExecuteDetails } from '../../__fixtures__'; import { defaultWatch } from '../../public/application/models/watch'; -import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; +import { setupEnvironment, pageHelpers, wrapBodyResponse } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; import { WATCH } from './helpers/jest_constants'; @@ -19,19 +19,19 @@ describe(' create route', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateJsonTestBed; + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); server.restore(); }); describe('on component mount', () => { beforeEach(async () => { testBed = await setup(); - - await act(async () => { - const { component } = testBed; - await nextTick(); - component.update(); - }); + testBed.component.update(); }); test('should set the correct page title', () => { @@ -92,7 +92,6 @@ describe(' create route', () => { await act(async () => { actions.clickSubmitButton(); - await nextTick(); }); const latestRequest = server.requests[server.requests.length - 1]; @@ -141,9 +140,8 @@ describe(' create route', () => { await act(async () => { actions.clickSubmitButton(); - await nextTick(); - component.update(); }); + component.update(); expect(exists('sectionError')).toBe(true); expect(find('sectionError').text()).toContain(error.message); @@ -169,7 +167,6 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); const latestRequest = server.requests[server.requests.length - 1]; @@ -230,9 +227,8 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); - component.update(); }); + component.update(); const latestRequest = server.requests[server.requests.length - 1]; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 77e65dfd91c7..481f59093d7d 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -9,15 +9,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; + import { getExecuteDetails } from '../../__fixtures__'; import { WATCH_TYPES } from '../../common/constants'; -import { - setupEnvironment, - pageHelpers, - nextTick, - wrapBodyResponse, - unwrapBodyResponse, -} from './helpers'; +import { setupEnvironment, pageHelpers, wrapBodyResponse, unwrapBodyResponse } from './helpers'; import { WatchCreateThresholdTestBed } from './helpers/watch_create_threshold.helpers'; const WATCH_NAME = 'my_test_watch'; @@ -76,7 +71,9 @@ jest.mock('@elastic/eui', () => { // which does not produce a valid component wrapper EuiComboBox: (props: any) => ( { props.onChange([syntheticEvent['0']]); }} @@ -91,7 +88,12 @@ describe(' create route', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateThresholdTestBed; + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); server.restore(); }); @@ -99,7 +101,6 @@ describe(' create route', () => { beforeEach(async () => { testBed = await setup(); const { component } = testBed; - await nextTick(); component.update(); }); @@ -159,46 +160,60 @@ describe(' create route', () => { test('it should enable the Create button and render additional content with valid fields', async () => { const { form, find, component, exists } = testBed; - form.setInputValue('nameInput', 'my_test_watch'); - find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox - form.setInputValue('watchTimeFieldSelect', '@timestamp'); + expect(find('saveWatchButton').props().disabled).toBe(true); await act(async () => { - await nextTick(); - component.update(); + form.setInputValue('nameInput', 'my_test_watch'); + find('indicesComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox + form.setInputValue('watchTimeFieldSelect', '@timestamp'); }); + component.update(); - expect(find('saveWatchButton').props().disabled).toEqual(false); - + expect(find('saveWatchButton').props().disabled).toBe(false); expect(find('watchConditionTitle').text()).toBe('Match the following condition'); expect(exists('watchVisualizationChart')).toBe(true); expect(exists('watchActionsPanel')).toBe(true); }); - // Looks like there is an issue with using 'mockComboBox'. - describe.skip('watch conditions', () => { - beforeEach(() => { - const { form, find } = testBed; + describe('watch conditions', () => { + beforeEach(async () => { + const { form, find, component } = testBed; // Name, index and time fields are required before the watch condition expression renders - form.setInputValue('nameInput', 'my_test_watch'); - act(() => { - find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox + await act(async () => { + form.setInputValue('nameInput', 'my_test_watch'); + find('indicesComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox + form.setInputValue('watchTimeFieldSelect', '@timestamp'); }); - form.setInputValue('watchTimeFieldSelect', '@timestamp'); + component.update(); }); - test('should require a threshold value', () => { - const { form, find } = testBed; + test('should require a threshold value', async () => { + const { form, find, component } = testBed; + // Display the threshold pannel act(() => { find('watchThresholdButton').simulate('click'); + }); + component.update(); + + await act(async () => { // Provide invalid value form.setInputValue('watchThresholdInput', ''); + }); + + // We need to wait for the debounced validation to be triggered and update the DOM + jest.advanceTimersByTime(500); + component.update(); + + expect(form.getErrorsMessages()).toContain('A value is required.'); + + await act(async () => { // Provide valid value form.setInputValue('watchThresholdInput', '0'); }); - expect(form.getErrorsMessages()).toContain('A value is required.'); + component.update(); + // No need to wait as the validation errors are cleared whenever the field changes expect(form.getErrorsMessages().length).toEqual(0); }); }); @@ -209,14 +224,12 @@ describe(' create route', () => { const { form, find, component } = testBed; // Set up valid fields needed for actions component to render - form.setInputValue('nameInput', WATCH_NAME); - find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox - form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); - await act(async () => { - await nextTick(); - component.update(); + form.setInputValue('nameInput', WATCH_NAME); + find('indicesComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); + form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); }); + component.update(); }); test('should simulate a logging action', async () => { @@ -240,7 +253,6 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); // Verify request @@ -303,7 +315,6 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); // Verify request @@ -366,7 +377,6 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); // Verify request @@ -431,15 +441,14 @@ describe(' create route', () => { expect(exists('watchActionAccordion')).toBe(true); // Provide valid fields and verify - find('watchActionAccordion.mockComboBox').simulate('change', [ + find('watchActionAccordion.toEmailAddressInput').simulate('change', [ { label: EMAIL_RECIPIENT, value: EMAIL_RECIPIENT }, - ]); // Using mocked EuiComboBox + ]); form.setInputValue('emailSubjectInput', EMAIL_SUBJECT); form.setInputValue('emailBodyInput', EMAIL_BODY); await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); // Verify request @@ -532,7 +541,6 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); // Verify request @@ -621,7 +629,6 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); // Verify request @@ -702,7 +709,6 @@ describe(' create route', () => { await act(async () => { actions.clickSimulateButton(); - await nextTick(); }); // Verify request @@ -753,20 +759,66 @@ describe(' create route', () => { }); }); + describe('watch visualize data payload', () => { + test('should send the correct payload', async () => { + const { form, find, component } = testBed; + + // Set up required fields + await act(async () => { + form.setInputValue('nameInput', WATCH_NAME); + find('indicesComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); + form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); + }); + component.update(); + + const latestReqToGetVisualizeData = server.requests.find( + (req) => req.method === 'POST' && req.url === '/api/watcher/watch/visualize' + ); + if (!latestReqToGetVisualizeData) { + throw new Error(`No request found to fetch visualize data.`); + } + + const requestBody = unwrapBodyResponse(latestReqToGetVisualizeData.requestBody); + + expect(requestBody.watch).toEqual({ + id: requestBody.watch.id, // id is dynamic + name: 'my_test_watch', + type: 'threshold', + isNew: true, + isActive: true, + actions: [], + index: ['index1'], + timeField: '@timestamp', + triggerIntervalSize: 1, + triggerIntervalUnit: 'm', + aggType: 'count', + termSize: 5, + termOrder: 'desc', + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + hasTermsAgg: false, + threshold: 1000, + }); + + expect(requestBody.options.interval).toBeDefined(); + }); + }); + describe('form payload', () => { test('should send the correct payload', async () => { const { form, find, component, actions } = testBed; // Set up required fields - form.setInputValue('nameInput', WATCH_NAME); - find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox - form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); + await act(async () => { + form.setInputValue('nameInput', WATCH_NAME); + find('indicesComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); + form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); + }); + component.update(); await act(async () => { - await nextTick(); - component.update(); actions.clickSubmitButton(); - await nextTick(); }); // Verify request diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index e8782edc829a..1188cc8469a5 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -12,7 +12,7 @@ import { getRandomString } from '@kbn/test/jest'; import { getWatch } from '../../__fixtures__'; import { defaultWatch } from '../../public/application/models/watch'; -import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; +import { setupEnvironment, pageHelpers, wrapBodyResponse } from './helpers'; import { WatchEditTestBed } from './helpers/watch_edit.helpers'; import { WATCH } from './helpers/jest_constants'; @@ -41,7 +41,12 @@ describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchEditTestBed; + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); server.restore(); }); @@ -50,11 +55,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadWatchResponse(WATCH); testBed = await setup(); - - await act(async () => { - await nextTick(); - testBed.component.update(); - }); + testBed.component.update(); }); describe('on component mount', () => { @@ -87,7 +88,6 @@ describe('', () => { await act(async () => { actions.clickSubmitButton(); - await nextTick(); }); const latestRequest = server.requests[server.requests.length - 1]; @@ -141,12 +141,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadWatchResponse({ watch }); testBed = await setup(); - - await act(async () => { - const { component } = testBed; - await nextTick(); - component.update(); - }); + testBed.component.update(); }); describe('on component mount', () => { @@ -172,7 +167,6 @@ describe('', () => { await act(async () => { actions.clickSubmitButton(); - await nextTick(); }); const latestRequest = server.requests[server.requests.length - 1]; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts index c19ec62b9447..1b1b813617da 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import moment from 'moment'; import { getWatchHistory } from '../../__fixtures__'; import { ROUTES, WATCH_STATES, ACTION_STATES } from '../../common/constants'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { setupEnvironment, pageHelpers } from './helpers'; import { WatchStatusTestBed } from './helpers/watch_status.helpers'; import { WATCH } from './helpers/jest_constants'; @@ -43,7 +43,12 @@ describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchStatusTestBed; + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); server.restore(); }); @@ -53,11 +58,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadWatchHistoryResponse(watchHistoryItems); testBed = await setup(); - - await act(async () => { - await nextTick(); - testBed.component.update(); - }); + testBed.component.update(); }); test('should set the correct page title', () => { @@ -175,9 +176,8 @@ describe('', () => { await act(async () => { confirmButton!.click(); - await nextTick(); - component.update(); }); + component.update(); const latestRequest = server.requests[server.requests.length - 1]; diff --git a/x-pack/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts index 6c6d6f116965..093f34e70400 100644 --- a/x-pack/plugins/watcher/public/plugin.ts +++ b/x-pack/plugins/watcher/public/plugin.ts @@ -8,13 +8,11 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin, CoreStart, Capabilities } from 'kibana/public'; import { first, map, skip } from 'rxjs/operators'; - import { Subject, combineLatest } from 'rxjs'; + import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; - -import { LicenseStatus } from '../common/types/license_status'; - import { ILicense } from '../../licensing/public'; +import { LicenseStatus } from '../common/types/license_status'; import { PLUGIN } from '../common/constants'; import { Dependencies } from './types'; diff --git a/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/get_interval_type.test.ts b/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/get_interval_type.test.ts new file mode 100644 index 000000000000..a90876d1baf2 --- /dev/null +++ b/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/get_interval_type.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { getIntervalType } from './get_interval_type'; + +describe('get interval type', () => { + test('should detect fixed intervals', () => { + ['1ms', '1s', '1m', '1h', '1d', '21s', '7d'].forEach((interval) => { + const intervalDetected = getIntervalType(interval); + try { + expect(intervalDetected).toBe('fixed_interval'); + } catch (e) { + throw new Error( + `Expected [${interval}] to be a fixed interval but got [${intervalDetected}]` + ); + } + }); + }); + + test('should detect calendar intervals', () => { + ['1w', '1M', '1q', '1y'].forEach((interval) => { + const intervalDetected = getIntervalType(interval); + try { + expect(intervalDetected).toBe('calendar_interval'); + } catch (e) { + throw new Error( + `Expected [${interval}] to be a calendar interval but got [${intervalDetected}]` + ); + } + }); + }); +}); diff --git a/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/get_interval_type.ts b/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/get_interval_type.ts new file mode 100644 index 000000000000..5e23523a133c --- /dev/null +++ b/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/get_interval_type.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Since 8.x we use the "fixed_interval" or "calendar_interval" parameter instead + * of the less precise "interval". This helper parse the interval and return its type. + * @param interval Interval value (e.g. "1d", "1w"...) + */ +export const getIntervalType = (interval: string): 'fixed_interval' | 'calendar_interval' => { + // We will consider all interval as fixed except if they are + // weekly (w), monthly (M), quarterly (q) or yearly (y) + const intervalMetric = interval.charAt(interval.length - 1); + if (['w', 'M', 'q', 'y'].includes(intervalMetric)) { + return 'calendar_interval'; + } + return 'fixed_interval'; +}; diff --git a/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/index.ts b/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/index.ts new file mode 100644 index 000000000000..0bb505e4ea72 --- /dev/null +++ b/x-pack/plugins/watcher/server/models/watch/lib/get_interval_type/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getIntervalType } from './get_interval_type'; diff --git a/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js b/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js index 10ba68c3193a..60b2dd5a546b 100644 --- a/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js +++ b/x-pack/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js @@ -6,8 +6,10 @@ */ import { cloneDeep } from 'lodash'; + import { buildInput } from '../../../../common/lib/serialization'; import { AGG_TYPES } from '../../../../common/constants'; +import { getIntervalType } from '../lib/get_interval_type'; /* input.search.request.body.query.bool.filter.range @@ -22,17 +24,6 @@ function buildRange({ rangeFrom, rangeTo, timeField }) { }; } -function buildDateAgg({ field, interval, timeZone }) { - return { - date_histogram: { - field, - interval, - time_zone: timeZone, - min_doc_count: 1, - }, - }; -} - function buildAggsCount(body, dateAgg) { return { dateAgg, @@ -93,7 +84,7 @@ function buildAggs(body, { aggType, termField }, dateAgg) { } } -export function buildVisualizeQuery(watch, visualizeOptions) { +export function buildVisualizeQuery(watch, visualizeOptions, kibanaVersion) { const { index, timeWindowSize, @@ -117,11 +108,22 @@ export function buildVisualizeQuery(watch, visualizeOptions) { termOrder, }); const body = watchInput.search.request.body; - const dateAgg = buildDateAgg({ - field: watch.timeField, - interval: visualizeOptions.interval, - timeZone: visualizeOptions.timezone, - }); + const dateAgg = { + date_histogram: { + field: watch.timeField, + time_zone: visualizeOptions.timezone, + min_doc_count: 1, + }, + }; + + if (kibanaVersion.major < 8) { + // In 7.x we use the deprecated "interval" in date_histogram agg + dateAgg.date_histogram.interval = visualizeOptions.interval; + } else { + // From 8.x we use the more precise "fixed_interval" or "calendar_interval" + const intervalType = getIntervalType(visualizeOptions.interval); + dateAgg.date_histogram[intervalType] = visualizeOptions.interval; + } // override the query range body.query.bool.filter.range = buildRange({ diff --git a/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js b/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js index 5cc8a5535c8c..a20b83e83e3b 100644 --- a/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js +++ b/x-pack/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js @@ -48,8 +48,8 @@ export class ThresholdWatch extends BaseWatch { return serializeThresholdWatch(this); } - getVisualizeQuery(visualizeOptions) { - return buildVisualizeQuery(this, visualizeOptions); + getVisualizeQuery(visualizeOptions, kibanaVersion) { + return buildVisualizeQuery(this, visualizeOptions, kibanaVersion); } formatVisualizeData(results) { diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index aea8368c7bbe..52d77520183a 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; - +import { SemVer } from 'semver'; import { CoreStart, CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { PLUGIN, INDEX_NAMES } from '../common/constants'; @@ -27,17 +27,19 @@ export class WatcherServerPlugin implements Plugin { private readonly license: License; private readonly logger: Logger; - constructor(ctx: PluginInitializerContext) { + constructor(private ctx: PluginInitializerContext) { this.logger = ctx.logger.get(); this.license = new License(); } - setup({ http, getStartServices }: CoreSetup, { licensing, features }: SetupDependencies) { + setup({ http }: CoreSetup, { features }: SetupDependencies) { this.license.setup({ pluginName: PLUGIN.getI18nName(i18n), logger: this.logger, }); + const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); + const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, @@ -45,6 +47,7 @@ export class WatcherServerPlugin implements Plugin { lib: { handleEsError, }, + kibanaVersion, }; features.registerElasticsearchFeature({ diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts index 61836d0ebae4..60442bf43bd6 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts @@ -37,6 +37,7 @@ export function registerVisualizeRoute({ router, license, lib: { handleEsError }, + kibanaVersion, }: RouteDependencies) { router.post( { @@ -48,7 +49,7 @@ export function registerVisualizeRoute({ license.guardApiRoute(async (ctx, request, response) => { const watch = Watch.fromDownstreamJson(request.body.watch); const options = VisualizeOptions.fromDownstreamJson(request.body.options); - const body = watch.getVisualizeQuery(options); + const body = watch.getVisualizeQuery(options, kibanaVersion); try { const hits = await fetchVisualizeData(ctx.core.elasticsearch.client, watch.index, body); diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index c9d43528d9ff..87cd5e40c279 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SemVer } from 'semver'; import type { IRouter } from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -33,4 +34,5 @@ export interface RouteDependencies { lib: { handleEsError: typeof handleEsError; }; + kibanaVersion: SemVer; }