From ad3d87517f882dc0f32ec365692ba4dea502217c Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Thu, 13 Aug 2020 12:38:57 -0500 Subject: [PATCH 01/46] Add kibana-core-ui-designers team (#74970) --- .github/CODEOWNERS | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6863b91858ff66..1f076e3c840015 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,7 @@ /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app -/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui +/src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app @@ -70,7 +70,7 @@ # Canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas -/x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui +/x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas # Core UI @@ -80,7 +80,7 @@ /src/plugins/home/server/services/ @elastic/kibana-core-ui # Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon /src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui -/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui +/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers /src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui # Observability UIs @@ -165,14 +165,14 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform /x-pack/legacy/plugins/security/ @elastic/kibana-security -/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui +/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/legacy/plugins/spaces/ @elastic/kibana-security -/x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui +/x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/ @elastic/kibana-security -/x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui +/x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security -/x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui +/x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/test/api_integration/apis/security/ @elastic/kibana-security # Kibana Localization From 479a991b988cbfeefab8bb4754718e727b009009 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 13 Aug 2020 14:17:33 -0400 Subject: [PATCH 02/46] [ML] DF Analytics: allow failed job to be stopped by force via the UI (#74710) * allow force stop from ui if job is failed * update wording in confirm modal --- .../components/action_stop/index.ts | 2 + .../action_stop/stop_button_modal.tsx | 56 +++++++++++++++++++ .../action_stop/use_force_stop_action.ts | 38 +++++++++++++ .../components/analytics_list/use_actions.tsx | 16 +++++- 4 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button_modal.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_force_stop_action.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts index 858b6c70501b3d..ce03305e3d8593 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts @@ -5,3 +5,5 @@ */ export { StopButton } from './stop_button'; +export { StopButtonModal } from './stop_button_modal'; +export { useForceStopAction } from './use_force_stop_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button_modal.tsx new file mode 100644 index 00000000000000..387f42cc85ef30 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button_modal.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; + +import { ForceStopAction } from './use_force_stop_action'; + +export const StopButtonModal: FC = ({ + closeModal, + item, + forceStopAndCloseModal, +}) => { + return ( + <> + {item !== undefined && ( + + +

+ +

+
+
+ )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_force_stop_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_force_stop_action.ts new file mode 100644 index 00000000000000..5a4e7489487317 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/use_force_stop_action.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; +import { stopAnalytics } from '../../services/analytics_service'; + +export type ForceStopAction = ReturnType; +export const useForceStopAction = () => { + const [isModalVisible, setModalVisible] = useState(false); + + const [item, setItem] = useState(); + + const closeModal = () => setModalVisible(false); + const forceStopAndCloseModal = () => { + if (item !== undefined) { + setModalVisible(false); + stopAnalytics(item); + } + }; + + const openModal = (newItem: DataFrameAnalyticsListRow) => { + setItem(newItem); + setModalVisible(true); + }; + + return { + closeModal, + isModalVisible, + item, + openModal, + forceStopAndCloseModal, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index 373b9991d4d3c9..d355335039085f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -21,12 +21,13 @@ import { EditButtonFlyout, } from '../action_edit'; import { useStartAction, StartButton, StartButtonModal } from '../action_start'; -import { StopButton } from '../action_stop'; +import { StopButton, useForceStopAction, StopButtonModal } from '../action_stop'; import { getViewAction } from '../action_view'; import { isCompletedAnalyticsJob, isDataFrameAnalyticsRunning, + isDataFrameAnalyticsFailed, DataFrameAnalyticsListRow, } from './common'; @@ -53,11 +54,13 @@ export const useActions = ( const deleteAction = useDeleteAction(); const editAction = useEditAction(); const startAction = useStartAction(); + const stopAction = useForceStopAction(); /* eslint-disable react-hooks/rules-of-hooks */ modals = ( <> {startAction.isModalVisible && } + {stopAction.isModalVisible && } {deleteAction.isModalVisible && } {isEditActionFlyoutVisible(editAction) && } @@ -78,7 +81,10 @@ export const useActions = ( ...[ { render: (item: DataFrameAnalyticsListRow) => { - if (!isDataFrameAnalyticsRunning(item.stats.state)) { + if ( + !isDataFrameAnalyticsRunning(item.stats.state) && + !isDataFrameAnalyticsFailed(item.stats.state) + ) { return ( { if (canStartStopDataFrameAnalytics) { - stopAnalytics(item); + if (isDataFrameAnalyticsFailed(item.stats.state)) { + stopAction.openModal(item); + } else { + stopAnalytics(item); + } } }} /> From 250a0b17b03f8924462d484c2254a5af7d64f1ff Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 13 Aug 2020 11:41:52 -0700 Subject: [PATCH 03/46] attempt excluding a codeowners directory --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1f076e3c840015..5a8271302a72b9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -118,6 +118,7 @@ # Operations /src/dev/ @elastic/kibana-operations +!/src/dev/i18n/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations /src/optimize/ @elastic/kibana-operations /packages/*eslint*/ @elastic/kibana-operations From c34e30ed0b29938228659afc57c6ec1f155ad47b Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 13 Aug 2020 14:55:43 -0400 Subject: [PATCH 04/46] [Security Solution][Resolver] Graph Control Tests and Update Simulator Selectors (#74680) Co-authored-by: oatkiller --- .../resolver/test_utilities/extend_jest.ts | 65 ++++++ .../test_utilities/simulator/index.tsx | 97 +++----- .../simulator/mock_resolver.tsx | 15 +- .../resolver/view/clickthrough.test.tsx | 37 +-- .../resolver/view/graph_controls.test.tsx | 221 ++++++++++++++++++ .../public/resolver/view/graph_controls.tsx | 25 +- .../public/resolver/view/panel.test.tsx | 24 +- 7 files changed, 379 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index 9fc7af38beb42a..df8f32d15a7ab6 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -17,6 +17,7 @@ declare global { namespace jest { interface Matchers { toYieldEqualTo(expectedYield: T extends AsyncIterable ? E : never): Promise; + toYieldObjectEqualTo(expectedYield: unknown): Promise; } } } @@ -57,6 +58,70 @@ expect.extend({ } } + // Use `pass` as set in the above loop (or initialized to `false`) + // See https://jestjs.io/docs/en/expect#custom-matchers-api and https://jestjs.io/docs/en/expect#thisutils + const message = pass + ? () => + `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + + `Expected: not ${this.utils.printExpected(expected)}\n${ + this.utils.stringify(expected) !== this.utils.stringify(received[received.length - 1]!) + ? `Received: ${this.utils.printReceived(received[received.length - 1])}` + : '' + }` + : () => + `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\nCompared ${ + received.length + } yields.\n\n${received + .map( + (next, index) => + `yield ${index + 1}:\n\n${this.utils.printDiffOrStringify( + expected, + next, + 'Expected', + 'Received', + this.expand + )}` + ) + .join(`\n\n`)}`; + + return { message, pass }; + }, + /** + * A custom matcher that takes an async generator and compares each value it yields to an expected value. + * This uses the same equality logic as `toMatchObject`. + * If any yielded value equals the expected value, the matcher will pass. + * If the generator ends with none of the yielded values matching, it will fail. + */ + async toYieldObjectEqualTo( + this: jest.MatcherContext, + receivedIterable: AsyncIterable, + expected: T + ): Promise<{ pass: boolean; message: () => string }> { + // Used in printing out the pass or fail message + const matcherName = 'toSometimesYieldEqualTo'; + const options: jest.MatcherHintOptions = { + comment: 'deep equality with any yielded value', + isNot: this.isNot, + promise: this.promise, + }; + // The last value received: Used in printing the message + const received: T[] = []; + + // Set to true if the test passes. + let pass: boolean = false; + + // Async iterate over the iterable + for await (const next of receivedIterable) { + // keep track of all received values. Used in pass and fail messages + received.push(next); + // Use deep equals to compare the value to the expected value + if ((this.equals(next, expected), [this.utils.iterableEquality, this.utils.subsetEquality])) { + // If the value is equal, break + pass = true; + break; + } + } + // Use `pass` as set in the above loop (or initialized to `false`) // See https://jestjs.io/docs/en/expect#custom-matchers-api and https://jestjs.io/docs/en/expect#thisutils const message = pass diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index cae6a18576ebd4..355b53e3740925 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -14,8 +14,9 @@ import { spyMiddlewareFactory } from '../spy_middleware_factory'; import { resolverMiddlewareFactory } from '../../store/middleware'; import { resolverReducer } from '../../store/reducer'; import { MockResolver } from './mock_resolver'; -import { ResolverState, DataAccessLayer, SpyMiddleware } from '../../types'; +import { ResolverState, DataAccessLayer, SpyMiddleware, SideEffectSimulator } from '../../types'; import { ResolverAction } from '../../store/actions'; +import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory'; /** * Test a Resolver instance using jest, enzyme, and a mock data layer. @@ -43,6 +44,11 @@ export class Simulator { * This is used by `debugActions`. */ private readonly spyMiddleware: SpyMiddleware; + /** + * Simulator which allows you to explicitly simulate resize events and trigger animation frames + */ + private readonly sideEffectSimulator: SideEffectSimulator; + constructor({ dataAccessLayer, resolverComponentInstanceID, @@ -87,11 +93,14 @@ export class Simulator { // Used for `KibanaContextProvider` const coreStart: CoreStart = coreMock.createStart(); + this.sideEffectSimulator = sideEffectSimulatorFactory(); + // Render Resolver via the `MockResolver` component, using `enzyme`. this.wrapper = mount( { + return this.resolveWrapper(() => this.domNodes(`[data-test-subj="${selector}"]`)); } /** - * The icon element for the node detail title. + * Given a 'data-test-subj' selector, it will return the domNode */ - public nodeDetailViewTitleIcon(): ReactWrapper { - return this.domNodes('[data-test-subj="resolver:node-detail:title-icon"]'); + public testSubject(selector: string): ReactWrapper { + return this.domNodes(`[data-test-subj="${selector}"]`); } /** @@ -297,7 +266,7 @@ export class Simulator { public async resolveWrapper( wrapperFactory: () => ReactWrapper, predicate: (wrapper: ReactWrapper) => boolean = (wrapper) => wrapper.length > 0 - ): Promise { + ): Promise { for await (const wrapper of this.map(wrapperFactory)) { if (predicate(wrapper)) { return wrapper; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 7de7cf48e6039c..5d5a414761dbf8 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -6,7 +6,7 @@ /* eslint-disable react/display-name */ -import React, { useMemo, useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { Router } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { Provider } from 'react-redux'; @@ -17,7 +17,6 @@ import { ResolverState, SideEffectSimulator, ResolverProps } from '../../types'; import { ResolverAction } from '../../store/actions'; import { ResolverWithoutProviders } from '../../view/resolver_without_providers'; import { SideEffectContext } from '../../view/side_effect_context'; -import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory'; type MockResolverProps = { /** @@ -38,6 +37,10 @@ type MockResolverProps = { history: React.ComponentProps['history']; /** Pass a resolver store. See `storeFactory` and `mockDataAccessLayer` */ store: Store; + /** + * Pass the side effect simulator which handles animations and resizing. See `sideEffectSimulatorFactory` + */ + sideEffectSimulator: SideEffectSimulator; /** * All the props from `ResolverWithoutStore` can be passed. These aren't defaulted to anything (you might want to test what happens when they aren't present.) */ @@ -66,8 +69,6 @@ export const MockResolver = React.memo((props: MockResolverProps) => { setResolverElement(element); }, []); - const simulator: SideEffectSimulator = useMemo(() => sideEffectSimulatorFactory(), []); - // Resize the Resolver element to match the passed in props. Resolver is size dependent. useEffect(() => { if (resolverElement) { @@ -84,15 +85,15 @@ export const MockResolver = React.memo((props: MockResolverProps) => { return this; }, }; - simulator.controls.simulateElementResize(resolverElement, size); + props.sideEffectSimulator.controls.simulateElementResize(resolverElement, size); } - }, [props.rasterWidth, props.rasterHeight, simulator.controls, resolverElement]); + }, [props.rasterWidth, props.rasterHeight, props.sideEffectSimulator.controls, resolverElement]); return ( - + ({ - graphElements: simulator.graphElement().length, - graphLoadingElements: simulator.graphLoadingElement().length, - graphErrorElements: simulator.graphErrorElement().length, + graphElements: simulator.testSubject('resolver:graph').length, + graphLoadingElements: simulator.testSubject('resolver:graph:loading').length, + graphErrorElements: simulator.testSubject('resolver:graph:error').length, })) ).toYieldEqualTo({ // it should have 1 graph element, an no error or loading elements. @@ -72,8 +72,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', }); it(`should show links to the 3 nodes (with icons) in the node list.`, async () => { - await expect(simulator.map(() => simulator.nodeListNodeLinkText().length)).toYieldEqualTo(3); - await expect(simulator.map(() => simulator.nodeListNodeLinkIcons().length)).toYieldEqualTo(3); + await expect( + simulator.map(() => simulator.testSubject('resolver:node-list:node-link:title').length) + ).toYieldEqualTo(3); + await expect( + simulator.map(() => simulator.testSubject('resolver:node-list:node-link:title').length) + ).toYieldEqualTo(3); }); describe("when the second child node's first button has been clicked", () => { @@ -131,9 +135,9 @@ describe('Resolver, when analyzing a tree that has two related events for the or beforeEach(async () => { await expect( simulator.map(() => ({ - graphElements: simulator.graphElement().length, - graphLoadingElements: simulator.graphLoadingElement().length, - graphErrorElements: simulator.graphErrorElement().length, + graphElements: simulator.testSubject('resolver:graph').length, + graphLoadingElements: simulator.testSubject('resolver:graph:loading').length, + graphErrorElements: simulator.testSubject('resolver:graph:error').length, originNode: simulator.processNodeElements({ entityID: entityIDs.origin }).length, })) ).toYieldEqualTo({ @@ -147,7 +151,10 @@ describe('Resolver, when analyzing a tree that has two related events for the or it('should render a related events button', async () => { await expect( simulator.map(() => ({ - relatedEventButtons: simulator.processNodeRelatedEventButton(entityIDs.origin).length, + relatedEventButtons: simulator.processNodeChildElements( + entityIDs.origin, + 'resolver:submenu:button' + ).length, })) ).toYieldEqualTo({ relatedEventButtons: 1, @@ -156,7 +163,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or describe('when the related events button is clicked', () => { beforeEach(async () => { const button = await simulator.resolveWrapper(() => - simulator.processNodeRelatedEventButton(entityIDs.origin) + simulator.processNodeChildElements(entityIDs.origin, 'resolver:submenu:button') ); if (button) { button.simulate('click'); @@ -164,17 +171,19 @@ describe('Resolver, when analyzing a tree that has two related events for the or }); it('should open the submenu and display exactly one option with the correct count', async () => { await expect( - simulator.map(() => simulator.processNodeSubmenuItems().map((node) => node.text())) + simulator.map(() => + simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text()) + ) ).toYieldEqualTo(['2 registry']); await expect( - simulator.map(() => simulator.processNodeSubmenuItems().length) + simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').length) ).toYieldEqualTo(1); }); }); describe('and when the related events button is clicked again', () => { beforeEach(async () => { const button = await simulator.resolveWrapper(() => - simulator.processNodeRelatedEventButton(entityIDs.origin) + simulator.processNodeChildElements(entityIDs.origin, 'resolver:submenu:button') ); if (button) { button.simulate('click'); @@ -182,7 +191,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or }); it('should close the submenu', async () => { await expect( - simulator.map(() => simulator.processNodeSubmenuItems().length) + simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').length) ).toYieldEqualTo(0); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx new file mode 100644 index 00000000000000..6497cc2971980c --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Simulator } from '../test_utilities/simulator'; +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import { nudgeAnimationDuration } from '../store/camera/scaling_constants'; +import '../test_utilities/extend_jest'; + +describe('graph controls: when relsover is loaded with an origin node', () => { + let simulator: Simulator; + let originEntityID: string; + let originNodeStyle: () => AsyncIterable; + const resolverComponentInstanceID = 'graph-controls-test'; + + const originalPositionStyle: Readonly<{ left: string; top: string }> = { + left: '746.93132px', + top: '535.5792px', + }; + const originalSizeStyle: Readonly<{ width: string; height: string }> = { + width: '360px', + height: '120px', + }; + + beforeEach(async () => { + const { + metadata: { databaseDocumentID, entityIDs }, + dataAccessLayer, + } = noAncestorsTwoChildren(); + + simulator = new Simulator({ + dataAccessLayer, + databaseDocumentID, + resolverComponentInstanceID, + }); + originEntityID = entityIDs.origin; + + originNodeStyle = () => + simulator.map(() => { + const wrapper = simulator.processNodeElements({ entityID: originEntityID }); + // `getDOMNode` can only be called on a wrapper of a single node: https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/getDOMNode.html + if (wrapper.length === 1) { + return wrapper.getDOMNode().style; + } + return null; + }); + }); + + it('should display all cardinal panning buttons and the center button', async () => { + await expect( + simulator.map(() => ({ + westPanButton: simulator.testSubject('resolver:graph-controls:west-button').length, + southPanButton: simulator.testSubject('resolver:graph-controls:south-button').length, + eastPanButton: simulator.testSubject('resolver:graph-controls:east-button').length, + northPanButton: simulator.testSubject('resolver:graph-controls:north-button').length, + centerButton: simulator.testSubject('resolver:graph-controls:center-button').length, + })) + ).toYieldEqualTo({ + westPanButton: 1, + southPanButton: 1, + eastPanButton: 1, + northPanButton: 1, + centerButton: 1, + }); + }); + + it('should display the zoom buttons and slider', async () => { + await expect( + simulator.map(() => ({ + zoomInButton: simulator.testSubject('resolver:graph-controls:zoom-in').length, + zoomOutButton: simulator.testSubject('resolver:graph-controls:zoom-out').length, + zoomSlider: simulator.testSubject('resolver:graph-controls:zoom-slider').length, + })) + ).toYieldEqualTo({ + zoomInButton: 1, + zoomOutButton: 1, + zoomSlider: 1, + }); + }); + + it("should show the origin node in it's original position", async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalPositionStyle); + }); + + describe('when the user clicks the west panning button', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:west-button'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node further left on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '796.93132px', + top: '535.5792px', + }); + }); + }); + + describe('when the user clicks the south panning button', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:south-button'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node lower on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '746.93132px', + top: '485.5792px', + }); + }); + }); + + describe('when the user clicks the east panning button', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:east-button'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node further right on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '696.93132px', + top: '535.5792px', + }); + }); + }); + + describe('when the user clicks the north panning button', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:north-button'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node higher on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '746.93132px', + top: '585.5792px', + }); + }); + }); + + describe('when the user clicks the center panning button', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:north-button'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + (await simulator.resolve('resolver:graph-controls:center-button'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it("should return the origin node to it's original position", async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalPositionStyle); + }); + }); + + it('should show the origin node as larger on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalSizeStyle); + }); + + describe('when the zoom in button is clicked', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:zoom-in'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node as larger on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '427.7538290724795px', + height: '142.5846096908265px', + }); + }); + }); + + describe('when the zoom out button is clicked', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:zoom-out'))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node as smaller on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '303.0461709275204px', + height: '101.01539030917347px', + }); + }); + }); + + describe('when the slider is moved upwards', () => { + beforeEach(async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalSizeStyle); + + (await simulator.resolve('resolver:graph-controls:zoom-slider'))!.simulate('change', { + target: { value: 0.8 }, + }); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node as large on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '525.6000000000001px', + height: '175.20000000000005px', + }); + }); + }); + + describe('when the slider is moved downwards', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:zoom-slider'))!.simulate('change', { + target: { value: 0.2 }, + }); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); + + it('should show the origin node as smaller on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '201.60000000000002px', + height: '67.2px', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index c2a7bbaacbf1d4..610deef07775bc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -125,12 +125,13 @@ const GraphControlsComponent = React.memo( className={className} graphControlsBackground={colorMap.graphControlsBackground} graphControlsIconColor={colorMap.graphControls} + data-test-subj="resolver:graph-controls" >
- - diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 4d391a6c9ce59c..21b5a30ee9890c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -72,8 +72,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an it('should show the node details for the origin', async () => { await expect( simulator().map(() => { - const titleWrapper = simulator().nodeDetailViewTitle(); - const titleIconWrapper = simulator().nodeDetailViewTitleIcon(); + const titleWrapper = simulator().testSubject('resolver:node-detail:title'); + const titleIconWrapper = simulator().testSubject('resolver:node-detail:title-icon'); return { title: titleWrapper.exists() ? titleWrapper.text() : null, titleIcon: titleIconWrapper.exists() ? titleIconWrapper.text() : null, @@ -122,17 +122,17 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an }); it('should have 3 nodes (with icons) in the node list', async () => { - await expect(simulator().map(() => simulator().nodeListNodeLinkText().length)).toYieldEqualTo( - 3 - ); - await expect(simulator().map(() => simulator().nodeListNodeLinkIcons().length)).toYieldEqualTo( - 3 - ); + await expect( + simulator().map(() => simulator().testSubject('resolver:node-list:node-link:title').length) + ).toYieldEqualTo(3); + await expect( + simulator().map(() => simulator().testSubject('resolver:node-list:node-link:icon').length) + ).toYieldEqualTo(3); }); describe('when there is an item in the node list and its text has been clicked', () => { beforeEach(async () => { - const nodeLinks = await simulator().resolveWrapper(() => simulator().nodeListNodeLinkText()); + const nodeLinks = await simulator().resolve('resolver:node-list:node-link:title'); expect(nodeLinks).toBeTruthy(); if (nodeLinks) { nodeLinks.first().simulate('click'); @@ -158,8 +158,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an }); describe('and when the node list link has been clicked', () => { beforeEach(async () => { - const nodeListLink = await simulator().resolveWrapper(() => - simulator().nodeDetailBreadcrumbNodeListLink() + const nodeListLink = await simulator().resolve( + 'resolver:node-detail:breadcrumbs:node-list-link' ); if (nodeListLink) { nodeListLink.simulate('click'); @@ -169,7 +169,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an await expect( simulator().map(() => { return simulator() - .nodeListNodeLinkText() + .testSubject('resolver:node-list:node-link:title') .map((node) => node.text()); }) ).toYieldEqualTo(['c', 'd', 'e']); From fe017f52dd5b64ab8d5945cd8a4483a350651850 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 13 Aug 2020 13:38:16 -0600 Subject: [PATCH 05/46] Make data.search.aggs available on the server. (#74472) --- ...lugins-data-public.baseformatterspublic.md | 2 +- ...ibana-plugin-plugins-data-public.search.md | 8 +- ...in-plugins-data-server.aggconfigoptions.md | 13 + ...ugin-plugins-data-server.agggrouplabels.md | 15 + ...plugin-plugins-data-server.agggroupname.md | 11 + ...lugin-plugins-data-server.agggroupnames.md | 15 + ...ana-plugin-plugins-data-server.aggparam.md | 11 + ...gins-data-server.aggparamoption.display.md | 11 + ...gins-data-server.aggparamoption.enabled.md | 22 ++ ...ugin-plugins-data-server.aggparamoption.md | 25 ++ ...-plugins-data-server.aggparamoption.val.md | 11 + ...-data-server.aggparamtype._constructor_.md | 20 ++ ...ns-data-server.aggparamtype.allowedaggs.md | 11 + ...lugins-data-server.aggparamtype.makeagg.md | 11 + ...plugin-plugins-data-server.aggparamtype.md | 25 ++ ...plugin-plugins-data-server.bucket_types.md | 28 ++ ...a-plugin-plugins-data-server.iaggconfig.md | 15 + ...ana-plugin-plugins-data-server.iaggtype.md | 11 + ...gin-plugins-data-server.ifieldparamtype.md | 11 + ...ugin-plugins-data-server.imetricaggtype.md | 11 + ...n-plugins-data-server.isearchsetup.aggs.md | 11 + ...plugin-plugins-data-server.isearchsetup.md | 1 + ...n-plugins-data-server.isearchstart.aggs.md | 11 + ...plugin-plugins-data-server.isearchstart.md | 1 + .../kibana-plugin-plugins-data-server.md | 18 + ...plugin-plugins-data-server.metric_types.md | 38 +++ ...-server.optionedparamtype._constructor_.md | 20 ++ ...n-plugins-data-server.optionedparamtype.md | 24 ++ ...s-data-server.optionedparamtype.options.md | 11 + ...-data-server.optionedvalueprop.disabled.md | 11 + ...a-server.optionedvalueprop.iscompatible.md | 11 + ...n-plugins-data-server.optionedvalueprop.md | 21 ++ ...gins-data-server.optionedvalueprop.text.md | 11 + ...ins-data-server.optionedvalueprop.value.md | 11 + ...plugin-plugins-data-server.plugin.setup.md | 10 +- ...ibana-plugin-plugins-data-server.search.md | 20 ++ ...s-data-server.tabbedaggcolumn.aggconfig.md | 11 + ...-plugins-data-server.tabbedaggcolumn.id.md | 11 + ...gin-plugins-data-server.tabbedaggcolumn.md | 22 ++ ...lugins-data-server.tabbedaggcolumn.name.md | 11 + ...plugin-plugins-data-server.tabbedaggrow.md | 13 + ...plugins-data-server.tabbedtable.columns.md | 11 + ...-plugin-plugins-data-server.tabbedtable.md | 21 ++ ...in-plugins-data-server.tabbedtable.rows.md | 11 + .../new_platform/new_platform.karma_mock.js | 14 +- .../common/field_formats/converters/source.ts | 2 +- .../field_formats/field_formats_registry.ts | 2 +- .../data/common/field_formats/mocks.ts | 1 + src/plugins/data/common/index.ts | 2 - .../index_patterns/fields/field_list.ts | 3 +- .../fields/index_pattern_field.ts | 11 +- .../search/aggs/agg_config.test.ts | 5 +- .../search/aggs/agg_config.ts | 5 +- .../search/aggs/agg_configs.test.ts | 5 +- .../search/aggs/agg_configs.ts | 5 +- .../search/aggs/agg_groups.ts | 0 .../search/aggs/agg_params.test.ts | 0 .../search/aggs/agg_params.ts | 0 .../search/aggs/agg_type.test.ts | 0 .../search/aggs/agg_type.ts | 2 +- .../search/aggs/agg_types.ts | 86 +++-- .../search/aggs/agg_types_registry.test.ts | 60 ++-- .../search/aggs/agg_types_registry.ts | 46 ++- .../common/search/aggs/aggs_service.test.ts | 217 ++++++++++++ .../data/common/search/aggs/aggs_service.ts | 92 ++++++ .../search/aggs/buckets/_interval_options.ts | 0 .../_terms_other_bucket_helper.test.ts | 0 .../buckets/_terms_other_bucket_helper.ts | 0 .../search/aggs/buckets/bucket_agg_type.ts | 0 .../search/aggs/buckets/bucket_agg_types.ts | 0 .../create_filter/date_histogram.test.ts | 19 +- .../buckets/create_filter/date_histogram.ts | 0 .../buckets/create_filter/date_range.test.ts | 17 +- .../aggs/buckets/create_filter/date_range.ts | 0 .../buckets/create_filter/filters.test.ts | 12 +- .../aggs/buckets/create_filter/filters.ts | 0 .../buckets/create_filter/histogram.test.ts | 21 +- .../aggs/buckets/create_filter/histogram.ts | 12 +- .../buckets/create_filter/ip_range.test.ts | 3 +- .../aggs/buckets/create_filter/ip_range.ts | 0 .../aggs/buckets/create_filter/range.test.ts | 30 +- .../aggs/buckets/create_filter/range.ts | 12 +- .../aggs/buckets/create_filter/terms.test.ts | 3 +- .../aggs/buckets/create_filter/terms.ts | 0 .../search/aggs/buckets/date_histogram.ts | 41 ++- .../aggs/buckets/date_histogram_fn.test.ts | 0 .../search/aggs/buckets/date_histogram_fn.ts | 0 .../search/aggs/buckets/date_range.test.ts | 22 +- .../search/aggs/buckets/date_range.ts | 14 +- .../search/aggs/buckets/date_range_fn.test.ts | 0 .../search/aggs/buckets/date_range_fn.ts | 0 .../search/aggs/buckets/filter.ts | 0 .../search/aggs/buckets/filter_fn.test.ts | 0 .../search/aggs/buckets/filter_fn.ts | 0 .../search/aggs/buckets/filters.test.ts | 18 +- .../search/aggs/buckets/filters.ts | 11 +- .../search/aggs/buckets/filters_fn.test.ts | 0 .../search/aggs/buckets/filters_fn.ts | 0 .../search/aggs/buckets/geo_hash.test.ts | 0 .../search/aggs/buckets/geo_hash.ts | 0 .../search/aggs/buckets/geo_hash_fn.test.ts | 0 .../search/aggs/buckets/geo_hash_fn.ts | 0 .../search/aggs/buckets/geo_tile.ts | 0 .../search/aggs/buckets/geo_tile_fn.test.ts | 0 .../search/aggs/buckets/geo_tile_fn.ts | 0 .../search/aggs/buckets/histogram.test.ts | 32 +- .../search/aggs/buckets/histogram.ts | 22 +- .../search/aggs/buckets/histogram_fn.test.ts | 0 .../search/aggs/buckets/histogram_fn.ts | 0 .../search/aggs/buckets/index.ts | 1 + .../search/aggs/buckets/ip_range.ts | 0 .../search/aggs/buckets/ip_range_fn.test.ts | 0 .../search/aggs/buckets/ip_range_fn.ts | 0 .../search/aggs/buckets/lib/cidr_mask.test.ts | 0 .../search/aggs/buckets/lib/cidr_mask.ts | 2 +- .../search/aggs/buckets/lib/date_range.ts | 0 .../aggs/buckets/lib/extended_bounds.ts | 0 .../search/aggs/buckets/lib/geo_point.ts | 0 .../search/aggs/buckets/lib/ip_range.ts | 0 .../time_buckets/calc_auto_interval.test.ts | 0 .../lib/time_buckets/calc_auto_interval.ts | 0 .../lib/time_buckets/calc_es_interval.ts | 2 +- .../aggs/buckets/lib/time_buckets/index.ts | 0 .../lib/time_buckets/time_buckets.test.ts | 0 .../buckets/lib/time_buckets/time_buckets.ts | 2 +- .../buckets/migrate_include_exclude_format.ts | 0 .../search/aggs/buckets/range.test.ts | 18 +- .../search/aggs/buckets/range.ts | 14 +- .../search/aggs/buckets/range_fn.test.ts | 0 .../search/aggs/buckets/range_fn.ts | 0 .../search/aggs/buckets/range_key.ts | 0 .../aggs/buckets/significant_terms.test.ts | 3 +- .../search/aggs/buckets/significant_terms.ts | 0 .../aggs/buckets/significant_terms_fn.test.ts | 0 .../aggs/buckets/significant_terms_fn.ts | 0 .../search/aggs/buckets/terms.test.ts | 0 .../search/aggs/buckets/terms.ts | 0 .../search/aggs/buckets/terms_fn.test.ts | 0 .../search/aggs/buckets/terms_fn.ts | 0 .../search/aggs/index.test.ts | 32 +- src/plugins/data/common/search/aggs/index.ts | 14 +- .../search/aggs/metrics/avg.ts | 0 .../search/aggs/metrics/avg_fn.test.ts | 0 .../search/aggs/metrics/avg_fn.ts | 0 .../search/aggs/metrics/bucket_avg.ts | 0 .../search/aggs/metrics/bucket_avg_fn.test.ts | 0 .../search/aggs/metrics/bucket_avg_fn.ts | 0 .../search/aggs/metrics/bucket_max.ts | 0 .../search/aggs/metrics/bucket_max_fn.test.ts | 0 .../search/aggs/metrics/bucket_max_fn.ts | 0 .../search/aggs/metrics/bucket_min.ts | 0 .../search/aggs/metrics/bucket_min_fn.test.ts | 0 .../search/aggs/metrics/bucket_min_fn.ts | 0 .../search/aggs/metrics/bucket_sum.ts | 0 .../search/aggs/metrics/bucket_sum_fn.test.ts | 0 .../search/aggs/metrics/bucket_sum_fn.ts | 0 .../search/aggs/metrics/cardinality.ts | 0 .../aggs/metrics/cardinality_fn.test.ts | 0 .../search/aggs/metrics/cardinality_fn.ts | 0 .../search/aggs/metrics/count.ts | 0 .../search/aggs/metrics/count_fn.test.ts | 0 .../search/aggs/metrics/count_fn.ts | 0 .../search/aggs/metrics/cumulative_sum.ts | 0 .../aggs/metrics/cumulative_sum_fn.test.ts | 0 .../search/aggs/metrics/cumulative_sum_fn.ts | 0 .../search/aggs/metrics/derivative.ts | 0 .../search/aggs/metrics/derivative_fn.test.ts | 0 .../search/aggs/metrics/derivative_fn.ts | 0 .../search/aggs/metrics/geo_bounds.ts | 0 .../search/aggs/metrics/geo_bounds_fn.test.ts | 0 .../search/aggs/metrics/geo_bounds_fn.ts | 0 .../search/aggs/metrics/geo_centroid.ts | 0 .../aggs/metrics/geo_centroid_fn.test.ts | 0 .../search/aggs/metrics/geo_centroid_fn.ts | 0 .../search/aggs/metrics/index.ts | 0 .../lib/get_response_agg_config_class.ts | 0 .../metrics/lib/make_nested_label.test.ts | 0 .../aggs/metrics/lib/make_nested_label.ts | 0 .../aggs/metrics/lib/nested_agg_helpers.ts | 0 .../aggs/metrics/lib/ordinal_suffix.test.ts | 0 .../search/aggs/metrics/lib/ordinal_suffix.ts | 0 .../metrics/lib/parent_pipeline_agg_helper.ts | 0 .../metrics/lib/parent_pipeline_agg_writer.ts | 0 .../lib/sibling_pipeline_agg_helper.ts | 0 .../lib/sibling_pipeline_agg_writer.ts | 0 .../search/aggs/metrics/max.ts | 0 .../search/aggs/metrics/max_fn.test.ts | 0 .../search/aggs/metrics/max_fn.ts | 0 .../search/aggs/metrics/median.test.ts | 3 +- .../search/aggs/metrics/median.ts | 0 .../search/aggs/metrics/median_fn.test.ts | 0 .../search/aggs/metrics/median_fn.ts | 0 .../search/aggs/metrics/metric_agg_type.ts | 0 .../search/aggs/metrics/metric_agg_types.ts | 0 .../search/aggs/metrics/min.ts | 0 .../search/aggs/metrics/min_fn.test.ts | 0 .../search/aggs/metrics/min_fn.ts | 0 .../search/aggs/metrics/moving_avg.ts | 0 .../search/aggs/metrics/moving_avg_fn.test.ts | 0 .../search/aggs/metrics/moving_avg_fn.ts | 0 .../aggs/metrics/parent_pipeline.test.ts | 0 .../aggs/metrics/percentile_ranks.test.ts | 19 +- .../search/aggs/metrics/percentile_ranks.ts | 26 +- .../aggs/metrics/percentile_ranks_fn.test.ts | 0 .../aggs/metrics/percentile_ranks_fn.ts | 0 .../search/aggs/metrics/percentiles.test.ts | 2 +- .../search/aggs/metrics/percentiles.ts | 0 .../aggs/metrics/percentiles_fn.test.ts | 0 .../search/aggs/metrics/percentiles_fn.ts | 0 .../aggs/metrics/percentiles_get_value.ts | 0 .../search/aggs/metrics/serial_diff.ts | 0 .../aggs/metrics/serial_diff_fn.test.ts | 0 .../search/aggs/metrics/serial_diff_fn.ts | 0 .../aggs/metrics/sibling_pipeline.test.ts | 0 .../search/aggs/metrics/std_deviation.test.ts | 2 +- .../search/aggs/metrics/std_deviation.ts | 0 .../aggs/metrics/std_deviation_fn.test.ts | 0 .../search/aggs/metrics/std_deviation_fn.ts | 0 .../search/aggs/metrics/sum.ts | 0 .../search/aggs/metrics/sum_fn.test.ts | 0 .../search/aggs/metrics/sum_fn.ts | 0 .../search/aggs/metrics/top_hit.test.ts | 2 +- .../search/aggs/metrics/top_hit.ts | 0 .../search/aggs/metrics/top_hit_fn.test.ts | 0 .../search/aggs/metrics/top_hit_fn.ts | 0 .../search/aggs/param_types/agg.ts | 0 .../search/aggs/param_types/base.ts | 3 +- .../search/aggs/param_types/field.test.ts | 0 .../search/aggs/param_types/field.ts | 4 +- .../search/aggs/param_types/index.ts | 0 .../search/aggs/param_types/json.test.ts | 0 .../search/aggs/param_types/json.ts | 0 .../search/aggs/param_types/optioned.test.ts | 0 .../search/aggs/param_types/optioned.ts | 0 .../search/aggs/param_types/string.test.ts | 0 .../search/aggs/param_types/string.ts | 0 .../aggs/test_helpers/function_wrapper.ts | 0 .../search/aggs/test_helpers/index.ts | 4 +- .../test_helpers/mock_agg_types_registry.ts | 81 +++-- src/plugins/data/common/search/aggs/types.ts | 157 +++++++++ .../utils/calculate_auto_time_expression.ts | 15 +- .../date_histogram_interval.test.ts | 0 .../date_histogram_interval.ts | 0 .../{ => utils}/date_interval_utils/index.ts | 0 .../invalid_es_calendar_interval_error.ts | 0 .../invalid_es_interval_format_error.ts | 0 .../is_valid_es_interval.ts | 0 .../date_interval_utils/is_valid_interval.ts | 0 .../least_common_interval.test.ts | 0 .../least_common_interval.ts | 0 .../least_common_multiple.test.ts | 0 .../least_common_multiple.ts | 0 .../parse_es_interval.test.ts | 0 .../date_interval_utils/parse_es_interval.ts | 0 .../parse_interval.test.ts | 0 .../date_interval_utils/parse_interval.ts | 0 .../date_interval_utils/to_absolute_dates.ts | 2 +- .../aggs/utils/get_format_with_aggs.test.ts | 5 +- .../search/aggs/utils/get_format_with_aggs.ts | 9 +- .../search/aggs/utils/get_parsed_value.ts | 0 .../search/aggs/utils/index.ts | 2 + .../aggs/{ => utils}/ipv4_address.test.ts | 0 .../search/aggs/{ => utils}/ipv4_address.ts | 0 .../search/aggs/utils/prop_filter.test.ts | 0 .../search/aggs/utils/prop_filter.ts | 0 .../search/aggs/utils/to_angular_json.ts | 0 .../data/common/search/expressions/index.ts | 1 + .../utils/courier_inspector_stats.ts | 2 +- .../common/search/expressions/utils/index.ts | 20 ++ src/plugins/data/common/search/index.ts | 9 +- .../search/tabify/buckets.test.ts | 0 .../search/tabify/buckets.ts | 0 .../search/tabify/get_columns.test.ts | 0 .../search/tabify/get_columns.ts | 0 .../{public => common}/search/tabify/index.ts | 0 .../search/tabify/response_writer.test.ts | 0 .../search/tabify/response_writer.ts | 0 .../search/tabify/tabify.test.ts | 2 +- .../search/tabify/tabify.ts | 0 .../{public => common}/search/tabify/types.ts | 0 .../public/field_formats/utils/deserialize.ts | 2 +- src/plugins/data/public/index.ts | 62 ++-- src/plugins/data/public/mocks.ts | 2 +- src/plugins/data/public/plugin.ts | 19 +- src/plugins/data/public/public.api.md | 14 +- .../public/search/aggs/aggs_service.test.ts | 182 +++++++++++ .../data/public/search/aggs/aggs_service.ts | 152 +++++++++ src/plugins/data/public/search/aggs/index.ts | 11 +- src/plugins/data/public/search/aggs/mocks.ts | 19 +- src/plugins/data/public/search/aggs/types.ts | 130 +------- .../build_tabular_inspector_data.ts | 2 +- .../search/expressions/create_filter.test.ts | 14 +- .../search/expressions/create_filter.ts | 4 +- .../data/public/search/expressions/esaggs.ts | 14 +- .../public/search/expressions/utils/index.ts | 1 - .../expressions/utils/serialize_agg_config.ts | 2 +- src/plugins/data/public/search/index.ts | 2 - .../data/public/search/search_service.test.ts | 11 +- .../data/public/search/search_service.ts | 94 ++---- src/plugins/data/public/search/types.ts | 16 +- src/plugins/data/public/types.ts | 12 - src/plugins/data/server/index.ts | 51 +++ src/plugins/data/server/plugin.ts | 37 ++- .../server/search/aggs/aggs_service.test.ts | 113 +++++++ .../data/server/search/aggs/aggs_service.ts | 122 +++++++ src/plugins/data/server/search/aggs/index.ts | 21 ++ src/plugins/data/server/search/aggs/mocks.ts | 81 +++++ src/plugins/data/server/search/aggs/types.ts | 27 ++ src/plugins/data/server/search/index.ts | 2 + src/plugins/data/server/search/mocks.ts | 9 +- .../data/server/search/search_service.test.ts | 21 +- .../data/server/search/search_service.ts | 39 ++- src/plugins/data/server/search/types.ts | 3 + src/plugins/data/server/server.api.md | 309 ++++++++++++++++-- .../definitions/date_histogram.test.tsx | 10 +- 315 files changed, 2712 insertions(+), 831 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggconfigoptions.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggrouplabels.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupname.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupnames.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparam.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.display.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.enabled.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.val.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.allowedaggs.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.makeagg.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.bucket_types.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggconfig.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggtype.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldparamtype.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.imetricaggtype.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.aggs.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.aggs.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.options.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.disabled.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.iscompatible.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.text.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.value.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.aggconfig.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.id.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.name.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggrow.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.columns.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.rows.md rename src/plugins/data/{public => common}/search/aggs/agg_config.test.ts (98%) rename src/plugins/data/{public => common}/search/aggs/agg_config.ts (99%) rename src/plugins/data/{public => common}/search/aggs/agg_configs.test.ts (98%) rename src/plugins/data/{public => common}/search/aggs/agg_configs.ts (98%) rename src/plugins/data/{public => common}/search/aggs/agg_groups.ts (100%) rename src/plugins/data/{public => common}/search/aggs/agg_params.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/agg_params.ts (100%) rename src/plugins/data/{public => common}/search/aggs/agg_type.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/agg_type.ts (99%) rename src/plugins/data/{public => common}/search/aggs/agg_types.ts (66%) rename src/plugins/data/{public => common}/search/aggs/agg_types_registry.test.ts (54%) rename src/plugins/data/{public => common}/search/aggs/agg_types_registry.ts (57%) create mode 100644 src/plugins/data/common/search/aggs/aggs_service.test.ts create mode 100644 src/plugins/data/common/search/aggs/aggs_service.ts rename src/plugins/data/{public => common}/search/aggs/buckets/_interval_options.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/_terms_other_bucket_helper.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/_terms_other_bucket_helper.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/bucket_agg_type.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/bucket_agg_types.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/date_histogram.test.ts (87%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/date_histogram.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/date_range.test.ts (78%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/date_range.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/filters.test.ts (83%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/filters.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/histogram.test.ts (77%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/histogram.ts (80%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/ip_range.test.ts (96%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/ip_range.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/range.test.ts (73%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/range.ts (78%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/terms.test.ts (97%) rename src/plugins/data/{public => common}/search/aggs/buckets/create_filter/terms.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/date_histogram.ts (92%) rename src/plugins/data/{public => common}/search/aggs/buckets/date_histogram_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/date_histogram_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/date_range.test.ts (84%) rename src/plugins/data/{public => common}/search/aggs/buckets/date_range.ts (88%) rename src/plugins/data/{public => common}/search/aggs/buckets/date_range_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/date_range_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/filter.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/filter_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/filter_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/filters.test.ts (93%) rename src/plugins/data/{public => common}/search/aggs/buckets/filters.ts (90%) rename src/plugins/data/{public => common}/search/aggs/buckets/filters_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/filters_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/geo_hash.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/geo_hash.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/geo_hash_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/geo_hash_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/geo_tile.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/geo_tile_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/geo_tile_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/histogram.test.ts (90%) rename src/plugins/data/{public => common}/search/aggs/buckets/histogram.ts (93%) rename src/plugins/data/{public => common}/search/aggs/buckets/histogram_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/histogram_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/index.ts (97%) rename src/plugins/data/{public => common}/search/aggs/buckets/ip_range.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/ip_range_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/ip_range_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/cidr_mask.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/cidr_mask.ts (97%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/date_range.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/extended_bounds.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/geo_point.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/ip_range.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/time_buckets/calc_auto_interval.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts (97%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/time_buckets/index.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/lib/time_buckets/time_buckets.ts (99%) rename src/plugins/data/{public => common}/search/aggs/buckets/migrate_include_exclude_format.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/range.test.ts (79%) rename src/plugins/data/{public => common}/search/aggs/buckets/range.ts (90%) rename src/plugins/data/{public => common}/search/aggs/buckets/range_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/range_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/range_key.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/significant_terms.test.ts (95%) rename src/plugins/data/{public => common}/search/aggs/buckets/significant_terms.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/significant_terms_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/significant_terms_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/terms.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/terms.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/terms_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/buckets/terms_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/index.test.ts (63%) rename src/plugins/data/{public => common}/search/aggs/metrics/avg.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/avg_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/avg_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_avg.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_avg_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_avg_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_max.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_max_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_max_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_min.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_min_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_min_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_sum.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_sum_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/bucket_sum_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/cardinality.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/cardinality_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/cardinality_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/count.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/count_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/count_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/cumulative_sum.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/cumulative_sum_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/cumulative_sum_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/derivative.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/derivative_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/derivative_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/geo_bounds.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/geo_bounds_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/geo_bounds_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/geo_centroid.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/geo_centroid_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/geo_centroid_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/index.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/get_response_agg_config_class.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/make_nested_label.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/make_nested_label.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/nested_agg_helpers.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/ordinal_suffix.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/ordinal_suffix.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/parent_pipeline_agg_writer.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/lib/sibling_pipeline_agg_writer.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/max.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/max_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/max_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/median.test.ts (94%) rename src/plugins/data/{public => common}/search/aggs/metrics/median.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/median_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/median_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/metric_agg_type.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/metric_agg_types.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/min.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/min_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/min_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/moving_avg.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/moving_avg_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/moving_avg_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/parent_pipeline.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentile_ranks.test.ts (77%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentile_ranks.ts (86%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentile_ranks_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentile_ranks_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentiles.test.ts (96%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentiles.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentiles_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentiles_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/percentiles_get_value.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/serial_diff.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/serial_diff_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/serial_diff_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/sibling_pipeline.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/std_deviation.test.ts (97%) rename src/plugins/data/{public => common}/search/aggs/metrics/std_deviation.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/std_deviation_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/std_deviation_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/sum.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/sum_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/sum_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/top_hit.test.ts (99%) rename src/plugins/data/{public => common}/search/aggs/metrics/top_hit.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/top_hit_fn.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/metrics/top_hit_fn.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/agg.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/base.ts (96%) rename src/plugins/data/{public => common}/search/aggs/param_types/field.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/field.ts (96%) rename src/plugins/data/{public => common}/search/aggs/param_types/index.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/json.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/json.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/optioned.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/optioned.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/string.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/param_types/string.ts (100%) rename src/plugins/data/{public => common}/search/aggs/test_helpers/function_wrapper.ts (100%) rename src/plugins/data/{public => common}/search/aggs/test_helpers/index.ts (86%) rename src/plugins/data/{public => common}/search/aggs/test_helpers/mock_agg_types_registry.ts (52%) create mode 100644 src/plugins/data/common/search/aggs/types.ts rename src/plugins/data/{public => common}/search/aggs/utils/calculate_auto_time_expression.ts (71%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/date_histogram_interval.test.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/date_histogram_interval.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/index.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/invalid_es_calendar_interval_error.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/invalid_es_interval_format_error.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/is_valid_es_interval.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/is_valid_interval.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/least_common_interval.test.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/least_common_interval.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/least_common_multiple.test.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/least_common_multiple.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/parse_es_interval.test.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/parse_es_interval.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/parse_interval.test.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/parse_interval.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/date_interval_utils/to_absolute_dates.ts (95%) rename src/plugins/data/{public => common}/search/aggs/utils/get_format_with_aggs.test.ts (95%) rename src/plugins/data/{public => common}/search/aggs/utils/get_format_with_aggs.ts (95%) rename src/plugins/data/{public => common}/search/aggs/utils/get_parsed_value.ts (100%) rename src/plugins/data/{public => common}/search/aggs/utils/index.ts (93%) rename src/plugins/data/common/search/aggs/{ => utils}/ipv4_address.test.ts (100%) rename src/plugins/data/common/search/aggs/{ => utils}/ipv4_address.ts (100%) rename src/plugins/data/{public => common}/search/aggs/utils/prop_filter.test.ts (100%) rename src/plugins/data/{public => common}/search/aggs/utils/prop_filter.ts (100%) rename src/plugins/data/{public => common}/search/aggs/utils/to_angular_json.ts (100%) rename src/plugins/data/{public => common}/search/expressions/utils/courier_inspector_stats.ts (98%) create mode 100644 src/plugins/data/common/search/expressions/utils/index.ts rename src/plugins/data/{public => common}/search/tabify/buckets.test.ts (100%) rename src/plugins/data/{public => common}/search/tabify/buckets.ts (100%) rename src/plugins/data/{public => common}/search/tabify/get_columns.test.ts (100%) rename src/plugins/data/{public => common}/search/tabify/get_columns.ts (100%) rename src/plugins/data/{public => common}/search/tabify/index.ts (100%) rename src/plugins/data/{public => common}/search/tabify/response_writer.test.ts (100%) rename src/plugins/data/{public => common}/search/tabify/response_writer.ts (100%) rename src/plugins/data/{public => common}/search/tabify/tabify.test.ts (98%) rename src/plugins/data/{public => common}/search/tabify/tabify.ts (100%) rename src/plugins/data/{public => common}/search/tabify/types.ts (100%) create mode 100644 src/plugins/data/public/search/aggs/aggs_service.test.ts create mode 100644 src/plugins/data/public/search/aggs/aggs_service.ts create mode 100644 src/plugins/data/server/search/aggs/aggs_service.test.ts create mode 100644 src/plugins/data/server/search/aggs/aggs_service.ts create mode 100644 src/plugins/data/server/search/aggs/index.ts create mode 100644 src/plugins/data/server/search/aggs/mocks.ts create mode 100644 src/plugins/data/server/search/aggs/types.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md index 25f046983cbcee..1aa9f460c4fac8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md @@ -7,5 +7,5 @@ Signature: ```typescript -baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateNanosFormat | typeof DateFormat)[] +baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat | typeof DateNanosFormat)[] ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 6c8f7fbdb170b0..22dc92c275670d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -14,7 +14,7 @@ search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("./search/aggs/buckets/bucket_agg_type").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; } | { display: string; val: string; @@ -23,9 +23,9 @@ search: { InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; Ipv4Address: typeof Ipv4Address; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; - isNumberType: (agg: import("./search").AggConfig) => boolean; - isStringType: (agg: import("./search").AggConfig) => boolean; - isType: (...types: string[]) => (agg: import("./search").AggConfig) => boolean; + isNumberType: (agg: import("../common").AggConfig) => boolean; + isStringType: (agg: import("../common").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("../common").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parentPipelineType: string; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggconfigoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggconfigoptions.md new file mode 100644 index 00000000000000..effb2e798ad6fd --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggconfigoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggConfigOptions](./kibana-plugin-plugins-data-server.aggconfigoptions.md) + +## AggConfigOptions type + +Signature: + +```typescript +export declare type AggConfigOptions = Assign; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggrouplabels.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggrouplabels.md new file mode 100644 index 00000000000000..cf0caee6ac33e6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggrouplabels.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggGroupLabels](./kibana-plugin-plugins-data-server.agggrouplabels.md) + +## AggGroupLabels variable + +Signature: + +```typescript +AggGroupLabels: { + buckets: string; + metrics: string; + none: string; +} +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupname.md new file mode 100644 index 00000000000000..403294eba1367c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggGroupName](./kibana-plugin-plugins-data-server.agggroupname.md) + +## AggGroupName type + +Signature: + +```typescript +export declare type AggGroupName = $Values; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupnames.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupnames.md new file mode 100644 index 00000000000000..11d194723c5219 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.agggroupnames.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggGroupNames](./kibana-plugin-plugins-data-server.agggroupnames.md) + +## AggGroupNames variable + +Signature: + +```typescript +AggGroupNames: Readonly<{ + Buckets: "buckets"; + Metrics: "metrics"; + None: "none"; +}> +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparam.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparam.md new file mode 100644 index 00000000000000..893501666b9a07 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparam.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParam](./kibana-plugin-plugins-data-server.aggparam.md) + +## AggParam type + +Signature: + +```typescript +export declare type AggParam = BaseParamType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.display.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.display.md new file mode 100644 index 00000000000000..1030056e16afe4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.display.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) > [display](./kibana-plugin-plugins-data-server.aggparamoption.display.md) + +## AggParamOption.display property + +Signature: + +```typescript +display: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.enabled.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.enabled.md new file mode 100644 index 00000000000000..8b1fcc4a1bbd0e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.enabled.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) > [enabled](./kibana-plugin-plugins-data-server.aggparamoption.enabled.md) + +## AggParamOption.enabled() method + +Signature: + +```typescript +enabled?(agg: AggConfig): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| agg | AggConfig | | + +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.md new file mode 100644 index 00000000000000..a7ddcf395cab47 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) + +## AggParamOption interface + +Signature: + +```typescript +export interface AggParamOption +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [display](./kibana-plugin-plugins-data-server.aggparamoption.display.md) | string | | +| [val](./kibana-plugin-plugins-data-server.aggparamoption.val.md) | string | | + +## Methods + +| Method | Description | +| --- | --- | +| [enabled(agg)](./kibana-plugin-plugins-data-server.aggparamoption.enabled.md) | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.val.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.val.md new file mode 100644 index 00000000000000..2c87c91c294d94 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamoption.val.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) > [val](./kibana-plugin-plugins-data-server.aggparamoption.val.md) + +## AggParamOption.val property + +Signature: + +```typescript +val: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype._constructor_.md new file mode 100644 index 00000000000000..2e1b16855987e7 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) > [(constructor)](./kibana-plugin-plugins-data-server.aggparamtype._constructor_.md) + +## AggParamType.(constructor) + +Constructs a new instance of the `AggParamType` class + +Signature: + +```typescript +constructor(config: Record); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| config | Record<string, any> | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.allowedaggs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.allowedaggs.md new file mode 100644 index 00000000000000..36179a9ce35697 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.allowedaggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) > [allowedAggs](./kibana-plugin-plugins-data-server.aggparamtype.allowedaggs.md) + +## AggParamType.allowedAggs property + +Signature: + +```typescript +allowedAggs: string[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.makeagg.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.makeagg.md new file mode 100644 index 00000000000000..bd5d2fca776595 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.makeagg.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) > [makeAgg](./kibana-plugin-plugins-data-server.aggparamtype.makeagg.md) + +## AggParamType.makeAgg property + +Signature: + +```typescript +makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.md new file mode 100644 index 00000000000000..00c1906dd880bd --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggparamtype.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) + +## AggParamType class + +Signature: + +```typescript +export declare class AggParamType extends BaseParamType +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(config)](./kibana-plugin-plugins-data-server.aggparamtype._constructor_.md) | | Constructs a new instance of the AggParamType class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [allowedAggs](./kibana-plugin-plugins-data-server.aggparamtype.allowedaggs.md) | | string[] | | +| [makeAgg](./kibana-plugin-plugins-data-server.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.bucket_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.bucket_types.md new file mode 100644 index 00000000000000..568e435754545f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.bucket_types.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [BUCKET\_TYPES](./kibana-plugin-plugins-data-server.bucket_types.md) + +## BUCKET\_TYPES enum + +Signature: + +```typescript +export declare enum BUCKET_TYPES +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| DATE\_HISTOGRAM | "date_histogram" | | +| DATE\_RANGE | "date_range" | | +| FILTER | "filter" | | +| FILTERS | "filters" | | +| GEOHASH\_GRID | "geohash_grid" | | +| GEOTILE\_GRID | "geotile_grid" | | +| HISTOGRAM | "histogram" | | +| IP\_RANGE | "ip_range" | | +| RANGE | "range" | | +| SIGNIFICANT\_TERMS | "significant_terms" | | +| TERMS | "terms" | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggconfig.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggconfig.md new file mode 100644 index 00000000000000..261b6e0b3bac11 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggconfig.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IAggConfig](./kibana-plugin-plugins-data-server.iaggconfig.md) + +## IAggConfig type + + AggConfig + + This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. + +Signature: + +```typescript +export declare type IAggConfig = AggConfig; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggtype.md new file mode 100644 index 00000000000000..d5868e1b0917e0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iaggtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IAggType](./kibana-plugin-plugins-data-server.iaggtype.md) + +## IAggType type + +Signature: + +```typescript +export declare type IAggType = AggType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldparamtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldparamtype.md new file mode 100644 index 00000000000000..4937245647f4ed --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldparamtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldParamType](./kibana-plugin-plugins-data-server.ifieldparamtype.md) + +## IFieldParamType type + +Signature: + +```typescript +export declare type IFieldParamType = FieldParamType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.imetricaggtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.imetricaggtype.md new file mode 100644 index 00000000000000..ae779c2b1510f1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.imetricaggtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IMetricAggType](./kibana-plugin-plugins-data-server.imetricaggtype.md) + +## IMetricAggType type + +Signature: + +```typescript +export declare type IMetricAggType = MetricAggType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.aggs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.aggs.md new file mode 100644 index 00000000000000..86bd4ab694e113 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) > [aggs](./kibana-plugin-plugins-data-server.isearchsetup.aggs.md) + +## ISearchSetup.aggs property + +Signature: + +```typescript +aggs: AggsSetup; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md index d9749bc44f45a8..e5b11a0b997ea6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md @@ -14,6 +14,7 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | +| [aggs](./kibana-plugin-plugins-data-server.isearchsetup.aggs.md) | AggsSetup | | | [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | (name: string, strategy: ISearchStrategy) => void | Extension point exposed for other plugins to register their own search strategies. | | [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md) | SearchUsage | Used internally for telemetry | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.aggs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.aggs.md new file mode 100644 index 00000000000000..8da429a07708c8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) > [aggs](./kibana-plugin-plugins-data-server.isearchstart.aggs.md) + +## ISearchStart.aggs property + +Signature: + +```typescript +aggs: AggsStart; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index 308ce3cb568bc7..3762da963d4d9b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -14,6 +14,7 @@ export interface ISearchStart | Property | Type | Description | | --- | --- | --- | +| [aggs](./kibana-plugin-plugins-data-server.isearchstart.aggs.md) | AggsStart | | | [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | | [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | (context: RequestHandlerContext, request: IKibanaSearchRequest, options: ISearchOptions) => Promise<IKibanaSearchResponse> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index f472064c877555..0292e08063fbbd 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -8,15 +8,19 @@ | Class | Description | | --- | --- | +| [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | +| [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | ## Enumerations | Enumeration | Description | | --- | --- | +| [BUCKET\_TYPES](./kibana-plugin-plugins-data-server.bucket_types.md) | | | [ES\_FIELD\_TYPES](./kibana-plugin-plugins-data-server.es_field_types.md) | \* | | [KBN\_FIELD\_TYPES](./kibana-plugin-plugins-data-server.kbn_field_types.md) | \* | +| [METRIC\_TYPES](./kibana-plugin-plugins-data-server.metric_types.md) | | ## Functions @@ -33,6 +37,7 @@ | Interface | Description | | --- | --- | +| [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) | | | [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-server.fieldformatconfig.md) | | | [Filter](./kibana-plugin-plugins-data-server.filter.md) | | @@ -48,17 +53,22 @@ | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | | [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | | [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | | +| [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) | | | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | | [Query](./kibana-plugin-plugins-data-server.query.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-server.refreshinterval.md) | | | [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) | | +| [TabbedAggColumn](./kibana-plugin-plugins-data-server.tabbedaggcolumn.md) | \* | +| [TabbedTable](./kibana-plugin-plugins-data-server.tabbedtable.md) | \* | | [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | ## Variables | Variable | Description | | --- | --- | +| [AggGroupLabels](./kibana-plugin-plugins-data-server.agggrouplabels.md) | | +| [AggGroupNames](./kibana-plugin-plugins-data-server.agggroupnames.md) | | | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-server.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [config](./kibana-plugin-plugins-data-server.config.md) | | | [esFilters](./kibana-plugin-plugins-data-server.esfilters.md) | | @@ -73,8 +83,16 @@ | Type Alias | Description | | --- | --- | +| [AggConfigOptions](./kibana-plugin-plugins-data-server.aggconfigoptions.md) | | +| [AggGroupName](./kibana-plugin-plugins-data-server.agggroupname.md) | | +| [AggParam](./kibana-plugin-plugins-data-server.aggparam.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) | | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | +| [IAggConfig](./kibana-plugin-plugins-data-server.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | +| [IAggType](./kibana-plugin-plugins-data-server.iaggtype.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | +| [IFieldParamType](./kibana-plugin-plugins-data-server.ifieldparamtype.md) | | +| [IMetricAggType](./kibana-plugin-plugins-data-server.imetricaggtype.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | +| [TabbedAggRow](./kibana-plugin-plugins-data-server.tabbedaggrow.md) | \* | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md new file mode 100644 index 00000000000000..49df98b6d70a19 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [METRIC\_TYPES](./kibana-plugin-plugins-data-server.metric_types.md) + +## METRIC\_TYPES enum + +Signature: + +```typescript +export declare enum METRIC_TYPES +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| AVG | "avg" | | +| AVG\_BUCKET | "avg_bucket" | | +| CARDINALITY | "cardinality" | | +| COUNT | "count" | | +| CUMULATIVE\_SUM | "cumulative_sum" | | +| DERIVATIVE | "derivative" | | +| GEO\_BOUNDS | "geo_bounds" | | +| GEO\_CENTROID | "geo_centroid" | | +| MAX | "max" | | +| MAX\_BUCKET | "max_bucket" | | +| MEDIAN | "median" | | +| MIN | "min" | | +| MIN\_BUCKET | "min_bucket" | | +| MOVING\_FN | "moving_avg" | | +| PERCENTILE\_RANKS | "percentile_ranks" | | +| PERCENTILES | "percentiles" | | +| SERIAL\_DIFF | "serial_diff" | | +| STD\_DEV | "std_dev" | | +| SUM | "sum" | | +| SUM\_BUCKET | "sum_bucket" | | +| TOP\_HITS | "top_hits" | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype._constructor_.md new file mode 100644 index 00000000000000..3b2fd2218709ae --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) > [(constructor)](./kibana-plugin-plugins-data-server.optionedparamtype._constructor_.md) + +## OptionedParamType.(constructor) + +Constructs a new instance of the `OptionedParamType` class + +Signature: + +```typescript +constructor(config: Record); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| config | Record<string, any> | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.md new file mode 100644 index 00000000000000..6bf2ef4baa9154 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) + +## OptionedParamType class + +Signature: + +```typescript +export declare class OptionedParamType extends BaseParamType +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(config)](./kibana-plugin-plugins-data-server.optionedparamtype._constructor_.md) | | Constructs a new instance of the OptionedParamType class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [options](./kibana-plugin-plugins-data-server.optionedparamtype.options.md) | | OptionedValueProp[] | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.options.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.options.md new file mode 100644 index 00000000000000..868619ad5a9e06 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedparamtype.options.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) > [options](./kibana-plugin-plugins-data-server.optionedparamtype.options.md) + +## OptionedParamType.options property + +Signature: + +```typescript +options: OptionedValueProp[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.disabled.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.disabled.md new file mode 100644 index 00000000000000..e0a21a87276148 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.disabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) > [disabled](./kibana-plugin-plugins-data-server.optionedvalueprop.disabled.md) + +## OptionedValueProp.disabled property + +Signature: + +```typescript +disabled?: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.iscompatible.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.iscompatible.md new file mode 100644 index 00000000000000..de3ecc0b97a64a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.iscompatible.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) > [isCompatible](./kibana-plugin-plugins-data-server.optionedvalueprop.iscompatible.md) + +## OptionedValueProp.isCompatible property + +Signature: + +```typescript +isCompatible: (agg: IAggConfig) => boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.md new file mode 100644 index 00000000000000..ef2440035c83b6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) + +## OptionedValueProp interface + +Signature: + +```typescript +export interface OptionedValueProp +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [disabled](./kibana-plugin-plugins-data-server.optionedvalueprop.disabled.md) | boolean | | +| [isCompatible](./kibana-plugin-plugins-data-server.optionedvalueprop.iscompatible.md) | (agg: IAggConfig) => boolean | | +| [text](./kibana-plugin-plugins-data-server.optionedvalueprop.text.md) | string | | +| [value](./kibana-plugin-plugins-data-server.optionedvalueprop.value.md) | string | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.text.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.text.md new file mode 100644 index 00000000000000..0a2b3ac708038b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.text.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) > [text](./kibana-plugin-plugins-data-server.optionedvalueprop.text.md) + +## OptionedValueProp.text property + +Signature: + +```typescript +text: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.value.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.value.md new file mode 100644 index 00000000000000..76618558d0479a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.optionedvalueprop.value.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) > [value](./kibana-plugin-plugins-data-server.optionedvalueprop.value.md) + +## OptionedValueProp.value property + +Signature: + +```typescript +value: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index a6fdfdf6891c86..18fca3d2c8a669 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -7,10 +7,10 @@ Signature: ```typescript -setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { +setup(core: CoreSetup, { expressions, usageCollection }: DataPluginSetupDependencies): { search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; }; ``` @@ -19,15 +19,15 @@ setup(core: CoreSetup, { usageCollection }: DataPluginS | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup<object, DataPluginStart> | | -| { usageCollection } | DataPluginSetupDependencies | | +| core | CoreSetup<DataPluginStartDependencies, DataPluginStart> | | +| { expressions, usageCollection } | DataPluginSetupDependencies | | Returns: `{ search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md index 09563358100b3f..9e38ce3f64f381 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md @@ -9,15 +9,35 @@ ```typescript search: { aggs: { + CidrMask: typeof CidrMask; dateHistogramInterval: typeof dateHistogramInterval; + intervalOptions: ({ + display: string; + val: string; + enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + } | { + display: string; + val: string; + })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; Ipv4Address: typeof Ipv4Address; + isNumberType: (agg: import("../common").AggConfig) => boolean; + isStringType: (agg: import("../common").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("../common").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; + parentPipelineType: string; parseEsInterval: typeof parseEsInterval; parseInterval: typeof parseInterval; + propFilter: typeof propFilter; + siblingPipelineType: string; + termsAggFilter: string[]; toAbsoluteDates: typeof toAbsoluteDates; }; + getRequestInspectorStats: typeof getRequestInspectorStats; + getResponseInspectorStats: typeof getResponseInspectorStats; + tabifyAggResponse: typeof tabifyAggResponse; + tabifyGetColumns: typeof tabifyGetColumns; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.aggconfig.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.aggconfig.md new file mode 100644 index 00000000000000..9870f7380e16ac --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.aggconfig.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedAggColumn](./kibana-plugin-plugins-data-server.tabbedaggcolumn.md) > [aggConfig](./kibana-plugin-plugins-data-server.tabbedaggcolumn.aggconfig.md) + +## TabbedAggColumn.aggConfig property + +Signature: + +```typescript +aggConfig: IAggConfig; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.id.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.id.md new file mode 100644 index 00000000000000..4f5a964a07a0c5 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedAggColumn](./kibana-plugin-plugins-data-server.tabbedaggcolumn.md) > [id](./kibana-plugin-plugins-data-server.tabbedaggcolumn.id.md) + +## TabbedAggColumn.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.md new file mode 100644 index 00000000000000..5e47f745fa17ac --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedAggColumn](./kibana-plugin-plugins-data-server.tabbedaggcolumn.md) + +## TabbedAggColumn interface + +\* + +Signature: + +```typescript +export interface TabbedAggColumn +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [aggConfig](./kibana-plugin-plugins-data-server.tabbedaggcolumn.aggconfig.md) | IAggConfig | | +| [id](./kibana-plugin-plugins-data-server.tabbedaggcolumn.id.md) | string | | +| [name](./kibana-plugin-plugins-data-server.tabbedaggcolumn.name.md) | string | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.name.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.name.md new file mode 100644 index 00000000000000..8a07e2708066f5 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggcolumn.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedAggColumn](./kibana-plugin-plugins-data-server.tabbedaggcolumn.md) > [name](./kibana-plugin-plugins-data-server.tabbedaggcolumn.name.md) + +## TabbedAggColumn.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggrow.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggrow.md new file mode 100644 index 00000000000000..d592aeff89639a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedaggrow.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedAggRow](./kibana-plugin-plugins-data-server.tabbedaggrow.md) + +## TabbedAggRow type + +\* + +Signature: + +```typescript +export declare type TabbedAggRow = Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.columns.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.columns.md new file mode 100644 index 00000000000000..55f079c581c8b3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.columns.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedTable](./kibana-plugin-plugins-data-server.tabbedtable.md) > [columns](./kibana-plugin-plugins-data-server.tabbedtable.columns.md) + +## TabbedTable.columns property + +Signature: + +```typescript +columns: TabbedAggColumn[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.md new file mode 100644 index 00000000000000..1bb055a2a3ce95 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedTable](./kibana-plugin-plugins-data-server.tabbedtable.md) + +## TabbedTable interface + +\* + +Signature: + +```typescript +export interface TabbedTable +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [columns](./kibana-plugin-plugins-data-server.tabbedtable.columns.md) | TabbedAggColumn[] | | +| [rows](./kibana-plugin-plugins-data-server.tabbedtable.rows.md) | TabbedAggRow[] | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.rows.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.rows.md new file mode 100644 index 00000000000000..b783919a26573a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tabbedtable.rows.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TabbedTable](./kibana-plugin-plugins-data-server.tabbedtable.md) > [rows](./kibana-plugin-plugins-data-server.tabbedtable.rows.md) + +## TabbedTable.rows property + +Signature: + +```typescript +rows: TabbedAggRow[]; +``` diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 35380ada51a0ad..5ef1149146afe7 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -26,7 +26,7 @@ import { getAggTypes, AggConfigs, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../src/plugins/data/public/search/aggs'; +} from '../../../../../src/plugins/data/common/search/aggs'; import { ComponentRegistry } from '../../../../../src/plugins/advanced_settings/public/'; import { UI_SETTINGS } from '../../../../../src/plugins/data/public/'; import { @@ -184,12 +184,13 @@ const mockAggTypesRegistry = () => { const registry = new AggTypesRegistry(); const registrySetup = registry.setup(); const aggTypes = getAggTypes({ - uiSettings: mockCoreSetup.uiSettings, - query: querySetup, - getInternalStartServices: () => ({ - fieldFormats: getFieldFormatsRegistry(mockCoreStart), - notifications: mockCoreStart.notifications, + calculateBounds: sinon.fake(), + getConfig: sinon.fake(), + getFieldFormatsStart: () => ({ + deserialize: sinon.fake(), + getDefaultInstance: sinon.fake(), }), + isDefaultTimezone: () => true, }); aggTypes.buckets.forEach((type) => registrySetup.registerBucket(type)); aggTypes.metrics.forEach((type) => registrySetup.registerMetric(type)); @@ -240,7 +241,6 @@ export const npSetup = { query: querySetup, search: { aggs: { - calculateAutoTimeExpression: sinon.fake(), types: aggTypesRegistry.setup(), }, __LEGACY: { diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.ts index f00261e00971aa..9c81bb011e127e 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.ts @@ -22,7 +22,7 @@ import { shortenDottedString } from '../../utils'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; -import { UI_SETTINGS } from '../../'; +import { UI_SETTINGS } from '../../constants'; /** * Remove all of the whitespace between html tags diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 84bedd2f9dee07..4b847ebc358d70 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -33,7 +33,7 @@ import { baseFormatters } from './constants/base_formatters'; import { FieldFormat } from './field_format'; import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types'; -import { UI_SETTINGS } from '../'; +import { UI_SETTINGS } from '../constants'; export class FieldFormatsRegistry { protected fieldFormats: Map = new Map(); diff --git a/src/plugins/data/common/field_formats/mocks.ts b/src/plugins/data/common/field_formats/mocks.ts index 9bbaefe2d146a8..ddf9cf246ec7d5 100644 --- a/src/plugins/data/common/field_formats/mocks.ts +++ b/src/plugins/data/common/field_formats/mocks.ts @@ -24,6 +24,7 @@ export const fieldFormatsMock: IFieldFormatsRegistry = { getByFieldType: jest.fn(), getDefaultConfig: jest.fn(), getDefaultInstance: jest.fn().mockImplementation(() => ({ + convert: jest.fn().mockImplementation((t: string) => t), getConverterFor: jest.fn().mockImplementation(() => (t: string) => t), })) as any, getDefaultInstanceCacheResolver: jest.fn(), diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index ca6bc965d48c53..bc7080e7d450b7 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -25,7 +25,5 @@ export * from './index_patterns'; export * from './kbn_field_types'; export * from './query'; export * from './search'; -export * from './search/aggs'; -export * from './search/expressions'; export * from './types'; export * from './utils'; diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index 172da9f9ca43f2..34bd69230a2e46 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -18,10 +18,11 @@ */ import { findIndex } from 'lodash'; -import { IFieldType, shortenDottedString } from '../../../common'; +import { IFieldType } from './types'; import { IndexPatternField } from './index_pattern_field'; import { OnNotification, FieldSpec } from '../types'; import { IndexPattern } from '../index_patterns'; +import { shortenDottedString } from '../../utils'; type FieldMap = Map; diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 679de103f8019d..965f1a7f630651 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -18,13 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { - IFieldType, - KbnFieldType, - getKbnFieldType, - KBN_FIELD_TYPES, - FieldFormat, -} from '../../../common'; +import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; +import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; +import { FieldFormat } from '../../field_formats'; +import { IFieldType } from './types'; import { OnNotification, FieldSpec } from '../types'; import { IndexPattern } from '../index_patterns'; diff --git a/src/plugins/data/public/search/aggs/agg_config.test.ts b/src/plugins/data/common/search/aggs/agg_config.test.ts similarity index 98% rename from src/plugins/data/public/search/aggs/agg_config.test.ts rename to src/plugins/data/common/search/aggs/agg_config.test.ts index f9279e06d14a9d..a443eacee731c4 100644 --- a/src/plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/plugins/data/common/search/aggs/agg_config.test.ts @@ -25,7 +25,8 @@ import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockAggTypesRegistry } from './test_helpers'; import { MetricAggType } from './metrics/metric_agg_type'; -import { IndexPattern, IIndexPatternFieldList } from '../../index_patterns'; +import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; +import { IIndexPatternFieldList } from '../../index_patterns/fields'; describe('AggConfig', () => { let indexPattern: IndexPattern; @@ -622,7 +623,7 @@ describe('AggConfig', () => { it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => { // Overwrite the `ranges` param in the `range` agg with a mock toExpressionAst function - const range: MetricAggType = typesRegistry.get('range'); + const range = typesRegistry.get('range') as MetricAggType; range.expressionName = 'aggRange'; const rangesParam = range.params.find((p) => p.name === 'ranges'); rangesParam!.toExpressionAst = (val: any) => ({ diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts similarity index 99% rename from src/plugins/data/public/search/aggs/agg_config.ts rename to src/plugins/data/common/search/aggs/agg_config.ts index 31618eac18e98f..b5747ce7bb9bdf 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -20,16 +20,17 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { Assign, Ensure } from '@kbn/utility-types'; + +import { FetchOptions, ISearchSource } from 'src/plugins/data/public'; import { ExpressionAstFunction, ExpressionAstArgument, SerializedFieldFormat, } from 'src/plugins/expressions/common'; + import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; -import { FetchOptions } from '../fetch'; -import { ISearchSource } from '../search_source'; type State = string | number | boolean | null | undefined | SerializableState; diff --git a/src/plugins/data/public/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts similarity index 98% rename from src/plugins/data/public/search/aggs/agg_configs.test.ts rename to src/plugins/data/common/search/aggs/agg_configs.test.ts index ff0cc3341929e2..803ccc70b98a76 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -22,8 +22,9 @@ import { AggConfig } from './agg_config'; import { AggConfigs } from './agg_configs'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockAggTypesRegistry } from './test_helpers'; -import { IndexPatternField, IndexPattern } from '../../index_patterns'; -import { stubIndexPattern, stubIndexPatternWithFields } from '../../../public/stubs'; +import type { IndexPatternField } from '../../index_patterns'; +import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; +import { stubIndexPattern, stubIndexPatternWithFields } from '../../../common/stubs'; describe('AggConfigs', () => { let indexPattern: IndexPattern; diff --git a/src/plugins/data/public/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts similarity index 98% rename from src/plugins/data/public/search/aggs/agg_configs.ts rename to src/plugins/data/common/search/aggs/agg_configs.ts index b272dfd3c7468e..203eda3a907eeb 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -20,13 +20,12 @@ import _ from 'lodash'; import { Assign } from '@kbn/utility-types'; +import { FetchOptions, ISearchSource } from 'src/plugins/data/public'; import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; -import { IndexPattern } from '../../index_patterns'; -import { ISearchSource } from '../search_source'; -import { FetchOptions } from '../fetch'; +import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; import { TimeRange } from '../../../common'; function removeParentAggs(obj: any) { diff --git a/src/plugins/data/public/search/aggs/agg_groups.ts b/src/plugins/data/common/search/aggs/agg_groups.ts similarity index 100% rename from src/plugins/data/public/search/aggs/agg_groups.ts rename to src/plugins/data/common/search/aggs/agg_groups.ts diff --git a/src/plugins/data/public/search/aggs/agg_params.test.ts b/src/plugins/data/common/search/aggs/agg_params.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/agg_params.test.ts rename to src/plugins/data/common/search/aggs/agg_params.test.ts diff --git a/src/plugins/data/public/search/aggs/agg_params.ts b/src/plugins/data/common/search/aggs/agg_params.ts similarity index 100% rename from src/plugins/data/public/search/aggs/agg_params.ts rename to src/plugins/data/common/search/aggs/agg_params.ts diff --git a/src/plugins/data/public/search/aggs/agg_type.test.ts b/src/plugins/data/common/search/aggs/agg_type.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/agg_type.test.ts rename to src/plugins/data/common/search/aggs/agg_type.test.ts diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts similarity index 99% rename from src/plugins/data/public/search/aggs/agg_type.ts rename to src/plugins/data/common/search/aggs/agg_type.ts index de7ca48e71d578..0ba2bb66e77581 100644 --- a/src/plugins/data/public/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -20,6 +20,7 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { ISearchSource } from 'src/plugins/data/public'; import { SerializedFieldFormat } from 'src/plugins/expressions/common'; import type { RequestAdapter } from 'src/plugins/inspector/common'; @@ -28,7 +29,6 @@ import { AggConfig } from './agg_config'; import { IAggConfigs } from './agg_configs'; import { BaseParamType } from './param_types/base'; import { AggParamType } from './param_types/agg'; -import { ISearchSource } from '../search_source'; export interface AggTypeConfig< TAggConfig extends AggConfig = AggConfig, diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts similarity index 66% rename from src/plugins/data/public/search/aggs/agg_types.ts rename to src/plugins/data/common/search/aggs/agg_types.ts index 2820ae495f3187..8565de13aed5b6 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IUiSettingsClient } from 'src/core/public'; -import { TimeRange, TimeRangeBounds } from '../../../common'; -import { GetInternalStartServicesFn } from '../../types'; +import { FieldFormatsStartCommon } from '../../field_formats'; +import { BUCKET_TYPES } from './buckets'; +import { METRIC_TYPES } from './metrics'; import { getCountMetricAgg } from './metrics/count'; import { getAvgMetricAgg } from './metrics/avg'; @@ -39,7 +39,7 @@ import { getCumulativeSumMetricAgg } from './metrics/cumulative_sum'; import { getMovingAvgMetricAgg } from './metrics/moving_avg'; import { getSerialDiffMetricAgg } from './metrics/serial_diff'; -import { getDateHistogramBucketAgg } from './buckets/date_histogram'; +import { getDateHistogramBucketAgg, CalculateBoundsFn } from './buckets/date_histogram'; import { getHistogramBucketAgg } from './buckets/histogram'; import { getRangeBucketAgg } from './buckets/range'; import { getDateRangeBucketAgg } from './buckets/date_range'; @@ -55,52 +55,50 @@ import { getBucketAvgMetricAgg } from './metrics/bucket_avg'; import { getBucketMinMetricAgg } from './metrics/bucket_min'; import { getBucketMaxMetricAgg } from './metrics/bucket_max'; +/** @internal */ export interface AggTypesDependencies { - calculateBounds: (timeRange: TimeRange) => TimeRangeBounds; - getInternalStartServices: GetInternalStartServicesFn; - uiSettings: IUiSettingsClient; + calculateBounds: CalculateBoundsFn; + getConfig: (key: string) => T; + getFieldFormatsStart: () => Pick; + isDefaultTimezone: () => boolean; } -export const getAggTypes = ({ - calculateBounds, - getInternalStartServices, - uiSettings, -}: AggTypesDependencies) => ({ +export const getAggTypes = () => ({ metrics: [ - getCountMetricAgg(), - getAvgMetricAgg(), - getSumMetricAgg(), - getMedianMetricAgg(), - getMinMetricAgg(), - getMaxMetricAgg(), - getStdDeviationMetricAgg(), - getCardinalityMetricAgg(), - getPercentilesMetricAgg(), - getPercentileRanksMetricAgg({ getInternalStartServices }), - getTopHitMetricAgg(), - getDerivativeMetricAgg(), - getCumulativeSumMetricAgg(), - getMovingAvgMetricAgg(), - getSerialDiffMetricAgg(), - getBucketAvgMetricAgg(), - getBucketSumMetricAgg(), - getBucketMinMetricAgg(), - getBucketMaxMetricAgg(), - getGeoBoundsMetricAgg(), - getGeoCentroidMetricAgg(), + { name: METRIC_TYPES.COUNT, fn: getCountMetricAgg }, + { name: METRIC_TYPES.AVG, fn: getAvgMetricAgg }, + { name: METRIC_TYPES.SUM, fn: getSumMetricAgg }, + { name: METRIC_TYPES.MEDIAN, fn: getMedianMetricAgg }, + { name: METRIC_TYPES.MIN, fn: getMinMetricAgg }, + { name: METRIC_TYPES.MAX, fn: getMaxMetricAgg }, + { name: METRIC_TYPES.STD_DEV, fn: getStdDeviationMetricAgg }, + { name: METRIC_TYPES.CARDINALITY, fn: getCardinalityMetricAgg }, + { name: METRIC_TYPES.PERCENTILES, fn: getPercentilesMetricAgg }, + { name: METRIC_TYPES.PERCENTILE_RANKS, fn: getPercentileRanksMetricAgg }, + { name: METRIC_TYPES.TOP_HITS, fn: getTopHitMetricAgg }, + { name: METRIC_TYPES.DERIVATIVE, fn: getDerivativeMetricAgg }, + { name: METRIC_TYPES.CUMULATIVE_SUM, fn: getCumulativeSumMetricAgg }, + { name: METRIC_TYPES.MOVING_FN, fn: getMovingAvgMetricAgg }, + { name: METRIC_TYPES.SERIAL_DIFF, fn: getSerialDiffMetricAgg }, + { name: METRIC_TYPES.AVG_BUCKET, fn: getBucketAvgMetricAgg }, + { name: METRIC_TYPES.SUM_BUCKET, fn: getBucketSumMetricAgg }, + { name: METRIC_TYPES.MIN_BUCKET, fn: getBucketMinMetricAgg }, + { name: METRIC_TYPES.MAX_BUCKET, fn: getBucketMaxMetricAgg }, + { name: METRIC_TYPES.GEO_BOUNDS, fn: getGeoBoundsMetricAgg }, + { name: METRIC_TYPES.GEO_CENTROID, fn: getGeoCentroidMetricAgg }, ], buckets: [ - getDateHistogramBucketAgg({ calculateBounds, uiSettings }), - getHistogramBucketAgg({ uiSettings, getInternalStartServices }), - getRangeBucketAgg({ getInternalStartServices }), - getDateRangeBucketAgg({ uiSettings }), - getIpRangeBucketAgg(), - getTermsBucketAgg(), - getFilterBucketAgg(), - getFiltersBucketAgg({ uiSettings }), - getSignificantTermsBucketAgg(), - getGeoHashBucketAgg(), - getGeoTitleBucketAgg(), + { name: BUCKET_TYPES.DATE_HISTOGRAM, fn: getDateHistogramBucketAgg }, + { name: BUCKET_TYPES.HISTOGRAM, fn: getHistogramBucketAgg }, + { name: BUCKET_TYPES.RANGE, fn: getRangeBucketAgg }, + { name: BUCKET_TYPES.DATE_RANGE, fn: getDateRangeBucketAgg }, + { name: BUCKET_TYPES.IP_RANGE, fn: getIpRangeBucketAgg }, + { name: BUCKET_TYPES.TERMS, fn: getTermsBucketAgg }, + { name: BUCKET_TYPES.FILTER, fn: getFilterBucketAgg }, + { name: BUCKET_TYPES.FILTERS, fn: getFiltersBucketAgg }, + { name: BUCKET_TYPES.SIGNIFICANT_TERMS, fn: getSignificantTermsBucketAgg }, + { name: BUCKET_TYPES.GEOHASH_GRID, fn: getGeoHashBucketAgg }, + { name: BUCKET_TYPES.GEOTILE_GRID, fn: getGeoTitleBucketAgg }, ], }); diff --git a/src/plugins/data/public/search/aggs/agg_types_registry.test.ts b/src/plugins/data/common/search/aggs/agg_types_registry.test.ts similarity index 54% rename from src/plugins/data/public/search/aggs/agg_types_registry.test.ts rename to src/plugins/data/common/search/aggs/agg_types_registry.test.ts index 58d1a07d965e26..df3dddfcd9c6fd 100644 --- a/src/plugins/data/public/search/aggs/agg_types_registry.test.ts +++ b/src/plugins/data/common/search/aggs/agg_types_registry.test.ts @@ -17,21 +17,17 @@ * under the License. */ -import { - AggTypesRegistry, - AggTypesRegistrySetup, - AggTypesRegistryStart, -} from './agg_types_registry'; +import { AggTypesRegistry, AggTypesRegistrySetup } from './agg_types_registry'; import { BucketAggType } from './buckets/bucket_agg_type'; import { MetricAggType } from './metrics/metric_agg_type'; -const bucketType = { name: 'terms', type: 'bucket' } as BucketAggType; -const metricType = { name: 'count', type: 'metric' } as MetricAggType; +const bucketType = () => ({ name: 'terms', type: 'buckets' } as BucketAggType); +const metricType = () => ({ name: 'count', type: 'metrics' } as MetricAggType); describe('AggTypesRegistry', () => { let registry: AggTypesRegistry; let setup: AggTypesRegistrySetup; - let start: AggTypesRegistryStart; + let start: ReturnType; beforeEach(() => { registry = new AggTypesRegistry(); @@ -40,49 +36,53 @@ describe('AggTypesRegistry', () => { }); it('registerBucket adds new buckets', () => { - setup.registerBucket(bucketType); - expect(start.getBuckets()).toEqual([bucketType]); + setup.registerBucket('terms', bucketType); + expect(start.getAll().buckets).toEqual([bucketType]); }); it('registerBucket throws error when registering duplicate bucket', () => { expect(() => { - setup.registerBucket(bucketType); - setup.registerBucket(bucketType); + setup.registerBucket('terms', bucketType); + setup.registerBucket('terms', bucketType); }).toThrow(/already been registered with name: terms/); + + const fooBucket = () => ({ name: 'foo', type: 'buckets' } as BucketAggType); + const fooMetric = () => ({ name: 'foo', type: 'metrics' } as MetricAggType); + expect(() => { + setup.registerBucket('foo', fooBucket); + setup.registerMetric('foo', fooMetric); + }).toThrow(/already been registered with name: foo/); }); it('registerMetric adds new metrics', () => { - setup.registerMetric(metricType); - expect(start.getMetrics()).toEqual([metricType]); + setup.registerMetric('count', metricType); + expect(start.getAll().metrics).toEqual([metricType]); }); it('registerMetric throws error when registering duplicate metric', () => { expect(() => { - setup.registerMetric(metricType); - setup.registerMetric(metricType); + setup.registerMetric('count', metricType); + setup.registerMetric('count', metricType); }).toThrow(/already been registered with name: count/); + + const fooBucket = () => ({ name: 'foo', type: 'buckets' } as BucketAggType); + const fooMetric = () => ({ name: 'foo', type: 'metrics' } as MetricAggType); + expect(() => { + setup.registerMetric('foo', fooMetric); + setup.registerBucket('foo', fooBucket); + }).toThrow(/already been registered with name: foo/); }); it('gets either buckets or metrics by id', () => { - setup.registerBucket(bucketType); - setup.registerMetric(metricType); + setup.registerBucket('terms', bucketType); + setup.registerMetric('count', metricType); expect(start.get('terms')).toEqual(bucketType); expect(start.get('count')).toEqual(metricType); }); - it('getBuckets retrieves only buckets', () => { - setup.registerBucket(bucketType); - expect(start.getBuckets()).toEqual([bucketType]); - }); - - it('getMetrics retrieves only metrics', () => { - setup.registerMetric(metricType); - expect(start.getMetrics()).toEqual([metricType]); - }); - it('getAll returns all buckets and metrics', () => { - setup.registerBucket(bucketType); - setup.registerMetric(metricType); + setup.registerBucket('terms', bucketType); + setup.registerMetric('count', metricType); expect(start.getAll()).toEqual({ buckets: [bucketType], metrics: [metricType], diff --git a/src/plugins/data/public/search/aggs/agg_types_registry.ts b/src/plugins/data/common/search/aggs/agg_types_registry.ts similarity index 57% rename from src/plugins/data/public/search/aggs/agg_types_registry.ts rename to src/plugins/data/common/search/aggs/agg_types_registry.ts index 5a0c58120d810d..ce22fa840bafa1 100644 --- a/src/plugins/data/public/search/aggs/agg_types_registry.ts +++ b/src/plugins/data/common/search/aggs/agg_types_registry.ts @@ -19,9 +19,21 @@ import { BucketAggType } from './buckets/bucket_agg_type'; import { MetricAggType } from './metrics/metric_agg_type'; +import { AggTypesDependencies } from './agg_types'; export type AggTypesRegistrySetup = ReturnType; -export type AggTypesRegistryStart = ReturnType; +/** + * AggsCommonStart returns the _unitialized_ agg type providers, but in our + * real start contract we will need to return the initialized versions. + * So we need to provide the correct typings so they can be overwritten + * on client/server. + * + * @internal + */ +export interface AggTypesRegistryStart { + get: (id: string) => BucketAggType | MetricAggType; + getAll: () => { buckets: Array>; metrics: Array> }; +} export class AggTypesRegistry { private readonly bucketAggs = new Map(); @@ -29,17 +41,27 @@ export class AggTypesRegistry { setup = () => { return { - registerBucket: >(type: T): void => { - const { name } = type; - if (this.bucketAggs.get(name)) { - throw new Error(`Bucket agg has already been registered with name: ${name}`); + registerBucket: < + N extends string, + T extends (deps: AggTypesDependencies) => BucketAggType + >( + name: N, + type: T + ): void => { + if (this.bucketAggs.get(name) || this.metricAggs.get(name)) { + throw new Error(`Agg has already been registered with name: ${name}`); } this.bucketAggs.set(name, type); }, - registerMetric: >(type: T): void => { - const { name } = type; - if (this.metricAggs.get(name)) { - throw new Error(`Metric agg has already been registered with name: ${name}`); + registerMetric: < + N extends string, + T extends (deps: AggTypesDependencies) => MetricAggType + >( + name: N, + type: T + ): void => { + if (this.bucketAggs.get(name) || this.metricAggs.get(name)) { + throw new Error(`Agg has already been registered with name: ${name}`); } this.metricAggs.set(name, type); }, @@ -51,12 +73,6 @@ export class AggTypesRegistry { get: (name: string) => { return this.bucketAggs.get(name) || this.metricAggs.get(name); }, - getBuckets: () => { - return Array.from(this.bucketAggs.values()); - }, - getMetrics: () => { - return Array.from(this.metricAggs.values()); - }, getAll: () => { return { buckets: Array.from(this.bucketAggs.values()), diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts new file mode 100644 index 00000000000000..bcf2101704c805 --- /dev/null +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -0,0 +1,217 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + AggsCommonService, + AggsCommonSetupDependencies, + AggsCommonStartDependencies, +} from './aggs_service'; +import { AggTypesDependencies, getAggTypes } from './agg_types'; +import { BucketAggType } from './buckets/bucket_agg_type'; +import { MetricAggType } from './metrics/metric_agg_type'; + +describe('Aggs service', () => { + let service: AggsCommonService; + let setupDeps: AggsCommonSetupDependencies; + let startDeps: AggsCommonStartDependencies; + const aggTypesDependencies: AggTypesDependencies = { + calculateBounds: jest.fn(), + getFieldFormatsStart: jest.fn(), + getConfig: jest.fn(), + isDefaultTimezone: () => true, + }; + + beforeEach(() => { + service = new AggsCommonService(); + setupDeps = { + registerFunction: jest.fn(), + }; + startDeps = { + getConfig: jest.fn(), + }; + }); + + describe('setup()', () => { + test('exposes proper contract', () => { + const setup = service.setup(setupDeps); + expect(Object.keys(setup).length).toBe(1); + expect(setup).toHaveProperty('types'); + }); + + test('instantiates a new registry', () => { + const a = new AggsCommonService(); + const b = new AggsCommonService(); + const bSetupDeps = { + registerFunction: jest.fn(), + }; + + const aSetup = a.setup(setupDeps); + aSetup.types.registerBucket( + 'foo', + () => ({ name: 'foo', type: 'buckets' } as BucketAggType) + ); + const aStart = a.start(startDeps); + expect(aStart.types.getAll().buckets.map((t) => t(aggTypesDependencies).name)) + .toMatchInlineSnapshot(` + Array [ + "date_histogram", + "histogram", + "range", + "date_range", + "ip_range", + "terms", + "filter", + "filters", + "significant_terms", + "geohash_grid", + "geotile_grid", + "foo", + ] + `); + expect(aStart.types.getAll().metrics.map((t) => t(aggTypesDependencies).name)) + .toMatchInlineSnapshot(` + Array [ + "count", + "avg", + "sum", + "median", + "min", + "max", + "std_dev", + "cardinality", + "percentiles", + "percentile_ranks", + "top_hits", + "derivative", + "cumulative_sum", + "moving_avg", + "serial_diff", + "avg_bucket", + "sum_bucket", + "min_bucket", + "max_bucket", + "geo_bounds", + "geo_centroid", + ] + `); + + b.setup(bSetupDeps); + const bStart = b.start(startDeps); + expect(bStart.types.getAll().buckets.map((t) => t(aggTypesDependencies).name)) + .toMatchInlineSnapshot(` + Array [ + "date_histogram", + "histogram", + "range", + "date_range", + "ip_range", + "terms", + "filter", + "filters", + "significant_terms", + "geohash_grid", + "geotile_grid", + ] + `); + expect(bStart.types.getAll().metrics.map((t) => t(aggTypesDependencies).name)) + .toMatchInlineSnapshot(` + Array [ + "count", + "avg", + "sum", + "median", + "min", + "max", + "std_dev", + "cardinality", + "percentiles", + "percentile_ranks", + "top_hits", + "derivative", + "cumulative_sum", + "moving_avg", + "serial_diff", + "avg_bucket", + "sum_bucket", + "min_bucket", + "max_bucket", + "geo_bounds", + "geo_centroid", + ] + `); + }); + + test('registers default agg types', () => { + service.setup(setupDeps); + const start = service.start(startDeps); + + const aggTypes = getAggTypes(); + expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length); + expect(start.types.getAll().metrics.length).toBe(aggTypes.metrics.length); + }); + + test('merges default agg types with types registered during setup', () => { + const setup = service.setup(setupDeps); + setup.types.registerBucket( + 'foo', + () => ({ name: 'foo', type: 'buckets' } as BucketAggType) + ); + setup.types.registerMetric( + 'bar', + () => ({ name: 'bar', type: 'metrics' } as MetricAggType) + ); + const start = service.start(startDeps); + + const aggTypes = getAggTypes(); + expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length + 1); + expect(start.types.getAll().buckets.some((t) => t(aggTypesDependencies).name === 'foo')).toBe( + true + ); + expect(start.types.getAll().metrics.length).toBe(aggTypes.metrics.length + 1); + expect(start.types.getAll().metrics.some((t) => t(aggTypesDependencies).name === 'bar')).toBe( + true + ); + }); + + test('registers all agg type expression functions', () => { + service.setup(setupDeps); + const aggTypes = getAggTypes(); + expect(setupDeps.registerFunction).toHaveBeenCalledTimes( + aggTypes.buckets.length + aggTypes.metrics.length + ); + }); + }); + + describe('start()', () => { + test('exposes proper contract', () => { + const start = service.start(startDeps); + expect(Object.keys(start).length).toBe(3); + expect(start).toHaveProperty('calculateAutoTimeExpression'); + expect(start).toHaveProperty('createAggConfigs'); + expect(start).toHaveProperty('types'); + }); + + test('types registry returns uninitialized type providers', () => { + service.setup(setupDeps); + const start = service.start(startDeps); + expect(typeof start.types.get('terms')).toBe('function'); + expect(start.types.get('terms')(aggTypesDependencies).name).toBe('terms'); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts new file mode 100644 index 00000000000000..59c54fcce68381 --- /dev/null +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; +import { UI_SETTINGS } from '../../../common'; +import { + AggConfigs, + AggTypesRegistry, + getAggTypes, + getAggTypesFunctions, + getCalculateAutoTimeExpression, +} from './'; +import { AggsCommonSetup, AggsCommonStart } from './types'; + +/** @internal */ +export const aggsRequiredUiSettings = [ + 'dateFormat', + 'dateFormat:scaled', + 'dateFormat:tz', + UI_SETTINGS.HISTOGRAM_BAR_TARGET, + UI_SETTINGS.HISTOGRAM_MAX_BARS, + UI_SETTINGS.SEARCH_QUERY_LANGUAGE, + UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS, + UI_SETTINGS.QUERY_STRING_OPTIONS, + UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX, +]; + +/** @internal */ +export interface AggsCommonSetupDependencies { + registerFunction: ExpressionsServiceSetup['registerFunction']; +} + +/** @internal */ +export interface AggsCommonStartDependencies { + getConfig: (key: string) => T; +} + +/** + * The aggs service provides a means of modeling and manipulating the various + * Elasticsearch aggregations supported by Kibana, providing the ability to + * output the correct DSL when you are ready to send your request to ES. + */ +export class AggsCommonService { + private readonly aggTypesRegistry = new AggTypesRegistry(); + + public setup({ registerFunction }: AggsCommonSetupDependencies): AggsCommonSetup { + const aggTypesSetup = this.aggTypesRegistry.setup(); + + // register each agg type + const aggTypes = getAggTypes(); + aggTypes.buckets.forEach(({ name, fn }) => aggTypesSetup.registerBucket(name, fn)); + aggTypes.metrics.forEach(({ name, fn }) => aggTypesSetup.registerMetric(name, fn)); + + // register expression functions for each agg type + const aggFunctions = getAggTypesFunctions(); + aggFunctions.forEach((fn) => registerFunction(fn)); + + return { + types: aggTypesSetup, + }; + } + + public start({ getConfig }: AggsCommonStartDependencies): AggsCommonStart { + const aggTypesStart = this.aggTypesRegistry.start(); + + return { + calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + createAggConfigs: (indexPattern, configStates = [], schemas) => { + return new AggConfigs(indexPattern, configStates, { + typesRegistry: aggTypesStart, + }); + }, + types: aggTypesStart, + }; + } +} diff --git a/src/plugins/data/public/search/aggs/buckets/_interval_options.ts b/src/plugins/data/common/search/aggs/buckets/_interval_options.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/_interval_options.ts rename to src/plugins/data/common/search/aggs/buckets/_interval_options.ts diff --git a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts rename to src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts rename to src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts diff --git a/src/plugins/data/public/search/aggs/buckets/bucket_agg_type.ts b/src/plugins/data/common/search/aggs/buckets/bucket_agg_type.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/bucket_agg_type.ts rename to src/plugins/data/common/search/aggs/buckets/bucket_agg_type.ts diff --git a/src/plugins/data/public/search/aggs/buckets/bucket_agg_types.ts b/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/bucket_agg_types.ts rename to src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts similarity index 87% rename from src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts index 24a17b60566cc4..143d5498369003 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -22,32 +22,17 @@ import { createFilterDateHistogram } from './date_histogram'; import { intervalOptions } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; -import { - getDateHistogramBucketAgg, - DateHistogramBucketAggDependencies, - IBucketDateHistogramAggConfig, -} from '../date_histogram'; +import { IBucketDateHistogramAggConfig } from '../date_histogram'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { RangeFilter } from '../../../../../common'; -import { coreMock } from '../../../../../../../core/public/mocks'; describe('AggConfig Filters', () => { describe('date_histogram', () => { - let aggTypesDependencies: DateHistogramBucketAggDependencies; let agg: IBucketDateHistogramAggConfig; let filter: RangeFilter; let bucketStart: any; let field: any; - beforeEach(() => { - const { uiSettings } = coreMock.createSetup(); - - aggTypesDependencies = { - calculateBounds: jest.fn(), - uiSettings, - }; - }); - const init = (interval: string = 'auto', duration: any = moment.duration(15, 'minutes')) => { field = { name: 'date', @@ -71,7 +56,7 @@ describe('AggConfig Filters', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getDateHistogramBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry(), } ); const bucketKey = 1422579600000; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.test.ts similarity index 78% rename from src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/date_range.test.ts index c272c037c5927e..8def27f1d8ee54 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.test.ts @@ -18,31 +18,18 @@ */ import moment from 'moment'; -import { getDateRangeBucketAgg, DateRangeBucketAggDependencies } from '../date_range'; import { createFilterDateRange } from './date_range'; -import { FieldFormatsGetConfigFn } from '../../../../../common'; -import { DateFormat } from '../../../../field_formats'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { coreMock } from '../../../../../../../core/public/mocks'; describe('AggConfig Filters', () => { describe('Date range', () => { - let aggTypesDependencies: DateRangeBucketAggDependencies; - - beforeEach(() => { - const { uiSettings } = coreMock.createSetup(); - - aggTypesDependencies = { uiSettings }; - }); - - const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { name: '@timestamp', - format: new DateFormat({}, getConfig), + format: {}, }; const indexPattern = { @@ -66,7 +53,7 @@ describe('AggConfig Filters', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getDateRangeBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry(), } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/create_filter/date_range.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/filters.test.ts similarity index 83% rename from src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/filters.test.ts index ff66d80c6d8d09..aec99bd00af550 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/filters.test.ts @@ -17,23 +17,13 @@ * under the License. */ -import { getFiltersBucketAgg, FiltersBucketAggDependencies } from '../filters'; import { createFilterFilters } from './filters'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { coreMock } from '../../../../../../../core/public/mocks'; describe('AggConfig Filters', () => { describe('filters', () => { - let aggTypesDependencies: FiltersBucketAggDependencies; - - beforeEach(() => { - const { uiSettings } = coreMock.createSetup(); - - aggTypesDependencies = { uiSettings }; - }); - const getAggConfigs = () => { const field = { name: 'bytes', @@ -63,7 +53,7 @@ describe('AggConfig Filters', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getFiltersBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry(), } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/filters.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/filters.ts diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts similarity index 77% rename from src/plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts index 3f9f5dd5672f0a..b57d530ef40e8d 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts @@ -17,25 +17,14 @@ * under the License. */ -import { createFilterHistogram } from './histogram'; +import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common/field_formats'; import { AggConfigs } from '../../agg_configs'; -import { mockAggTypesRegistry } from '../../test_helpers'; +import { mockAggTypesRegistry, mockGetFieldFormatsStart } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common'; -import { GetInternalStartServicesFn, InternalStartServices } from '../../../../types'; -import { FieldFormatsStart } from '../../../../field_formats'; -import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; +import { createFilterHistogram } from './histogram'; describe('AggConfig Filters', () => { - let getInternalStartServices: GetInternalStartServicesFn; - let fieldFormats: FieldFormatsStart; - - beforeEach(() => { - fieldFormats = fieldFormatsServiceMock.createStartContract(); - getInternalStartServices = () => (({ fieldFormats } as unknown) as InternalStartServices); - }); - describe('histogram', () => { const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { @@ -72,12 +61,12 @@ describe('AggConfig Filters', () => { test('should return an range filter for histogram', () => { const aggConfigs = getAggConfigs(); - const filter = createFilterHistogram(getInternalStartServices)( + const filter = createFilterHistogram(mockGetFieldFormatsStart)( aggConfigs.aggs[0] as IBucketAggConfig, '2048' ); - expect(fieldFormats.deserialize).toHaveBeenCalledTimes(1); + expect(mockGetFieldFormatsStart().deserialize).toHaveBeenCalledTimes(1); expect(filter).toHaveProperty('meta'); expect(filter.meta).toHaveProperty('index', '1234'); expect(filter).toHaveProperty('range'); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.ts similarity index 80% rename from src/plugins/data/public/search/aggs/buckets/create_filter/histogram.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/histogram.ts index f3626bc9130ad3..4684b1640cd82d 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.ts @@ -17,14 +17,16 @@ * under the License. */ -import { IBucketAggConfig } from '../bucket_agg_type'; import { buildRangeFilter, RangeFilterParams } from '../../../../../common'; -import { GetInternalStartServicesFn } from '../../../../types'; +import { AggTypesDependencies } from '../../agg_types'; +import { IBucketAggConfig } from '../bucket_agg_type'; /** @internal */ -export const createFilterHistogram = (getInternalStartServices: GetInternalStartServicesFn) => { +export const createFilterHistogram = ( + getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart'] +) => { return (aggConfig: IBucketAggConfig, key: string) => { - const { fieldFormats } = getInternalStartServices(); + const { deserialize } = getFieldFormatsStart(); const value = parseInt(key, 10); const params: RangeFilterParams = { gte: value, lt: value + aggConfig.params.interval }; @@ -32,7 +34,7 @@ export const createFilterHistogram = (getInternalStartServices: GetInternalStart aggConfig.params.field, params, aggConfig.getIndexPattern(), - fieldFormats.deserialize(aggConfig.toSerializedFieldFormat()).convert(key) + deserialize(aggConfig.toSerializedFieldFormat()).convert(key) ); }; }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.test.ts similarity index 96% rename from src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.test.ts index 852685a505afd0..9f823001aac8c1 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { getIpRangeBucketAgg } from '../ip_range'; import { createFilterIpRange } from './ip_range'; import { AggConfigs, CreateAggConfigParams } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; @@ -27,7 +26,7 @@ import { IBucketAggConfig } from '../bucket_agg_type'; describe('AggConfig Filters', () => { describe('IP range', () => { - const typesRegistry = mockAggTypesRegistry([getIpRangeBucketAgg()]); + const typesRegistry = mockAggTypesRegistry(); const getAggConfigs = (aggs: CreateAggConfigParams[]) => { const field = { name: 'ip', diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.ts diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts similarity index 73% rename from src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts index faffad3beb8c12..30af970f55aa9a 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts @@ -17,31 +17,15 @@ * under the License. */ -import { getRangeBucketAgg } from '../range'; -import { createFilterRange } from './range'; -import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common'; +import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common/field_formats'; import { AggConfigs } from '../../agg_configs'; -import { mockAggTypesRegistry } from '../../test_helpers'; -import { BUCKET_TYPES } from '../bucket_agg_types'; +import { mockAggTypesRegistry, mockGetFieldFormatsStart } from '../../test_helpers'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { FieldFormatsStart } from '../../../../field_formats'; -import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; -import { GetInternalStartServicesFn, InternalStartServices } from '../../../../types'; +import { BUCKET_TYPES } from '../bucket_agg_types'; +import { createFilterRange } from './range'; describe('AggConfig Filters', () => { describe('range', () => { - let getInternalStartServices: GetInternalStartServicesFn; - let fieldFormats: FieldFormatsStart; - - beforeEach(() => { - fieldFormats = fieldFormatsServiceMock.createStartContract(); - - getInternalStartServices = () => - (({ - fieldFormats, - } as unknown) as InternalStartServices); - }); - const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -72,14 +56,14 @@ describe('AggConfig Filters', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getRangeBucketAgg({ getInternalStartServices })]), + typesRegistry: mockAggTypesRegistry(), } ); }; test('should return a range filter for range agg', () => { const aggConfigs = getAggConfigs(); - const filter = createFilterRange(getInternalStartServices)( + const filter = createFilterRange(mockGetFieldFormatsStart)( aggConfigs.aggs[0] as IBucketAggConfig, { gte: 1024, @@ -87,7 +71,7 @@ describe('AggConfig Filters', () => { } ); - expect(fieldFormats.deserialize).toHaveBeenCalledTimes(1); + expect(mockGetFieldFormatsStart().deserialize).toHaveBeenCalledTimes(1); expect(filter).toHaveProperty('range'); expect(filter).toHaveProperty('meta'); expect(filter.meta).toHaveProperty('index', '1234'); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts similarity index 78% rename from src/plugins/data/public/search/aggs/buckets/create_filter/range.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/range.ts index f9db2973af1369..8dea33a450c5de 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts @@ -17,19 +17,21 @@ * under the License. */ -import { IBucketAggConfig } from '../bucket_agg_type'; import { buildRangeFilter } from '../../../../../common'; -import { GetInternalStartServicesFn } from '../../../../types'; +import { AggTypesDependencies } from '../../agg_types'; +import { IBucketAggConfig } from '../bucket_agg_type'; /** @internal */ -export const createFilterRange = (getInternalStartServices: GetInternalStartServicesFn) => { +export const createFilterRange = ( + getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart'] +) => { return (aggConfig: IBucketAggConfig, params: any) => { - const { fieldFormats } = getInternalStartServices(); + const { deserialize } = getFieldFormatsStart(); return buildRangeFilter( aggConfig.params.field, params, aggConfig.getIndexPattern(), - fieldFormats.deserialize(aggConfig.toSerializedFieldFormat()).convert(params) + deserialize(aggConfig.toSerializedFieldFormat()).convert(params) ); }; }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.test.ts similarity index 97% rename from src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/terms.test.ts index 1c165f0d29ab6e..c3c661296e1cf0 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { getTermsBucketAgg } from '../terms'; import { createFilterTerms } from './terms'; import { AggConfigs, CreateAggConfigParams } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; @@ -43,7 +42,7 @@ describe('AggConfig Filters', () => { }; return new AggConfigs(indexPattern, aggs, { - typesRegistry: mockAggTypesRegistry([getTermsBucketAgg()]), + typesRegistry: mockAggTypesRegistry(), }); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/create_filter/terms.ts rename to src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts similarity index 92% rename from src/plugins/data/public/search/aggs/buckets/date_histogram.ts rename to src/plugins/data/common/search/aggs/buckets/date_histogram.ts index fa1725eccbd286..fdf9c456b38760 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -20,27 +20,23 @@ import { get, noop, find, every } from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { IUiSettingsClient } from 'src/core/public'; -import { TimeBuckets } from './lib/time_buckets'; +import { KBN_FIELD_TYPES, TimeRange, TimeRangeBounds, UI_SETTINGS } from '../../../../common'; + +import { intervalOptions } from './_interval_options'; +import { createFilterDateHistogram } from './create_filter/date_histogram'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { createFilterDateHistogram } from './create_filter/date_histogram'; -import { intervalOptions } from './_interval_options'; +import { ExtendedBounds } from './lib/extended_bounds'; +import { TimeBuckets } from './lib/time_buckets'; + import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; - -import { - dateHistogramInterval, - KBN_FIELD_TYPES, - TimeRange, - TimeRangeBounds, - UI_SETTINGS, -} from '../../../../common'; import { BaseAggParams } from '../types'; -import { ExtendedBounds } from './lib/extended_bounds'; +import { dateHistogramInterval } from '../utils'; -type CalculateBoundsFn = (timeRange: TimeRange) => TimeRangeBounds; +/** @internal */ +export type CalculateBoundsFn = (timeRange: TimeRange) => TimeRangeBounds; const updateTimeBuckets = ( agg: IBucketDateHistogramAggConfig, @@ -58,7 +54,8 @@ const updateTimeBuckets = ( export interface DateHistogramBucketAggDependencies { calculateBounds: CalculateBoundsFn; - uiSettings: IUiSettingsClient; + isDefaultTimezone: () => boolean; + getConfig: (key: string) => T; } export interface IBucketDateHistogramAggConfig extends IBucketAggConfig { @@ -84,7 +81,8 @@ export interface AggParamsDateHistogram extends BaseAggParams { export const getDateHistogramBucketAgg = ({ calculateBounds, - uiSettings, + isDefaultTimezone, + getConfig, }: DateHistogramBucketAggDependencies) => new BucketAggType({ name: BUCKET_TYPES.DATE_HISTOGRAM, @@ -122,10 +120,10 @@ export const getDateHistogramBucketAgg = ({ if (buckets) return buckets; buckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + 'histogram:maxBars': getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': getConfig(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + dateFormat: getConfig('dateFormat'), + 'dateFormat:scaled': getConfig('dateFormat:scaled'), }); updateTimeBuckets(this, calculateBounds, buckets); @@ -252,10 +250,9 @@ export const getDateHistogramBucketAgg = ({ } if (!tz) { // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz - const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz'); const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); - tz = isDefaultTimezone ? detectedTimezone || tzOffset : uiSettings.get('dateFormat:tz'); + tz = isDefaultTimezone() ? detectedTimezone || tzOffset : getConfig('dateFormat:tz'); } output.params.time_zone = tz; }, diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/date_histogram_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/date_histogram_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts rename to src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts b/src/plugins/data/common/search/aggs/buckets/date_range.test.ts similarity index 84% rename from src/plugins/data/public/search/aggs/buckets/date_range.test.ts rename to src/plugins/data/common/search/aggs/buckets/date_range.test.ts index 69515dfee87fe1..66f8e269cd38d2 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.test.ts @@ -17,19 +17,20 @@ * under the License. */ -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { getDateRangeBucketAgg, DateRangeBucketAggDependencies } from './date_range'; import { AggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; +import { AggTypesDependencies } from '../agg_types'; +import { mockAggTypesRegistry, mockAggTypesDependencies } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; describe('date_range params', () => { - let aggTypesDependencies: DateRangeBucketAggDependencies; + let aggTypesDependencies: AggTypesDependencies; beforeEach(() => { - const { uiSettings } = coreMock.createSetup(); - - aggTypesDependencies = { uiSettings }; + aggTypesDependencies = { + ...mockAggTypesDependencies, + getConfig: jest.fn(), + isDefaultTimezone: jest.fn().mockReturnValue(false), + }; }); const getAggConfigs = (params: Record = {}, hasIncludeTypeMeta: boolean = true) => { @@ -68,7 +69,7 @@ describe('date_range params', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getDateRangeBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry(aggTypesDependencies), } ); }; @@ -108,10 +109,7 @@ describe('date_range params', () => { test('should use the Kibana time_zone if no parameter specified', () => { aggTypesDependencies = { ...aggTypesDependencies, - uiSettings: { - ...aggTypesDependencies.uiSettings, - get: () => 'kibanaTimeZone' as any, - }, + getConfig: () => 'kibanaTimeZone' as any, }; const aggConfigs = getAggConfigs( diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.ts b/src/plugins/data/common/search/aggs/buckets/date_range.ts similarity index 88% rename from src/plugins/data/public/search/aggs/buckets/date_range.ts rename to src/plugins/data/common/search/aggs/buckets/date_range.ts index 8c576023f02391..eda35a77afa5ff 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.ts @@ -20,14 +20,13 @@ import { get } from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { IUiSettingsClient } from 'src/core/public'; import { BUCKET_TYPES } from './bucket_agg_types'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterDateRange } from './create_filter/date_range'; import { DateRangeKey } from './lib/date_range'; -import { KBN_FIELD_TYPES } from '../../../../common'; +import { KBN_FIELD_TYPES } from '../../../../common/kbn_field_types/types'; import { BaseAggParams } from '../types'; const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', { @@ -35,7 +34,8 @@ const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', }); export interface DateRangeBucketAggDependencies { - uiSettings: IUiSettingsClient; + isDefaultTimezone: () => boolean; + getConfig: (key: string) => T; } export interface AggParamsDateRange extends BaseAggParams { @@ -44,7 +44,10 @@ export interface AggParamsDateRange extends BaseAggParams { time_zone?: string; } -export const getDateRangeBucketAgg = ({ uiSettings }: DateRangeBucketAggDependencies) => +export const getDateRangeBucketAgg = ({ + isDefaultTimezone, + getConfig, +}: DateRangeBucketAggDependencies) => new BucketAggType({ name: BUCKET_TYPES.DATE_RANGE, title: dateRangeTitle, @@ -100,9 +103,8 @@ export const getDateRangeBucketAgg = ({ uiSettings }: DateRangeBucketAggDependen if (!tz) { const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); - const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz'); - tz = isDefaultTimezone ? detectedTimezone || tzOffset : uiSettings.get('dateFormat:tz'); + tz = isDefaultTimezone() ? detectedTimezone || tzOffset : getConfig('dateFormat:tz'); } output.params.time_zone = tz; }, diff --git a/src/plugins/data/public/search/aggs/buckets/date_range_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/date_range_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/date_range_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/date_range_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts b/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/date_range_fn.ts rename to src/plugins/data/common/search/aggs/buckets/date_range_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/filter.ts b/src/plugins/data/common/search/aggs/buckets/filter.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/filter.ts rename to src/plugins/data/common/search/aggs/buckets/filter.ts diff --git a/src/plugins/data/public/search/aggs/buckets/filter_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/filter_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/filter_fn.ts b/src/plugins/data/common/search/aggs/buckets/filter_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/filter_fn.ts rename to src/plugins/data/common/search/aggs/buckets/filter_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/filters.test.ts b/src/plugins/data/common/search/aggs/buckets/filters.test.ts similarity index 93% rename from src/plugins/data/public/search/aggs/buckets/filters.test.ts rename to src/plugins/data/common/search/aggs/buckets/filters.test.ts index bcb82b5f99649a..f745b4537131a7 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters.test.ts @@ -18,20 +18,20 @@ */ import { Query } from '../../../../common'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; import { AggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; +import { AggTypesDependencies } from '../agg_types'; +import { mockAggTypesRegistry, mockAggTypesDependencies } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { getFiltersBucketAgg, FiltersBucketAggDependencies } from './filters'; describe('Filters Agg', () => { - let aggTypesDependencies: FiltersBucketAggDependencies; + let aggTypesDependencies: AggTypesDependencies; beforeEach(() => { jest.resetAllMocks(); - const { uiSettings } = coreMock.createSetup(); - - aggTypesDependencies = { uiSettings }; + aggTypesDependencies = { + ...mockAggTypesDependencies, + getConfig: jest.fn(), + }; }); describe('order agg editor UI', () => { @@ -61,7 +61,7 @@ describe('Filters Agg', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getFiltersBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry(aggTypesDependencies), } ); }; @@ -218,7 +218,7 @@ describe('Filters Agg', () => { }); test('works with leading wildcards if allowed', () => { - aggTypesDependencies.uiSettings.get = (s: any) => + aggTypesDependencies.getConfig = (s: any) => s === 'query:allowLeadingWildcards' ? true : s; const aggConfigs = getAggConfigs({ diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/common/search/aggs/buckets/filters.ts similarity index 90% rename from src/plugins/data/public/search/aggs/buckets/filters.ts rename to src/plugins/data/common/search/aggs/buckets/filters.ts index cd4ed721fda777..7310fa08b68e05 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters.ts @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { size, transform, cloneDeep } from 'lodash'; -import { IUiSettingsClient } from 'src/core/public'; import { createFilterFilters } from './create_filter/filters'; import { toAngularJSON } from '../utils'; @@ -41,7 +40,7 @@ interface FilterValue { } export interface FiltersBucketAggDependencies { - uiSettings: IUiSettingsClient; + getConfig: (key: string) => any; } export interface AggParamsFilters extends Omit { @@ -51,7 +50,7 @@ export interface AggParamsFilters extends Omit { }>; } -export const getFiltersBucketAgg = ({ uiSettings }: FiltersBucketAggDependencies) => +export const getFiltersBucketAgg = ({ getConfig }: FiltersBucketAggDependencies) => new BucketAggType({ name: BUCKET_TYPES.FILTERS, title: filtersTitle, @@ -60,9 +59,9 @@ export const getFiltersBucketAgg = ({ uiSettings }: FiltersBucketAggDependencies params: [ { name: 'filters', - default: [ + default: () => [ { - input: { query: '', language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) }, + input: { query: '', language: getConfig(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) }, label: '', }, ], @@ -80,7 +79,7 @@ export const getFiltersBucketAgg = ({ uiSettings }: FiltersBucketAggDependencies return; } - const esQueryConfigs = getEsQueryConfig(uiSettings); + const esQueryConfigs = getEsQueryConfig({ get: getConfig }); const query = buildEsQuery(aggConfig.getIndexPattern(), [input], [], esQueryConfigs); if (!query) { diff --git a/src/plugins/data/public/search/aggs/buckets/filters_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/filters_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/filters_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/filters_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/filters_fn.ts b/src/plugins/data/common/search/aggs/buckets/filters_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/filters_fn.ts rename to src/plugins/data/common/search/aggs/buckets/filters_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts rename to src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/geo_hash.ts rename to src/plugins/data/common/search/aggs/buckets/geo_hash.ts diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/geo_hash_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/geo_hash_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts rename to src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile.ts b/src/plugins/data/common/search/aggs/buckets/geo_tile.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/geo_tile.ts rename to src/plugins/data/common/search/aggs/buckets/geo_tile.ts diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/geo_tile_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/geo_tile_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts rename to src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts similarity index 90% rename from src/plugins/data/public/search/aggs/buckets/histogram.test.ts rename to src/plugins/data/common/search/aggs/buckets/histogram.test.ts index 6ac77f207d9cee..3727747984d3e1 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -17,33 +17,18 @@ * under the License. */ -import { coreMock } from '../../../../../../../src/core/public/mocks'; import { AggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; +import { mockAggTypesRegistry, mockAggTypesDependencies } from '../test_helpers'; +import { AggTypesDependencies } from '../agg_types'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { - IBucketHistogramAggConfig, - getHistogramBucketAgg, - AutoBounds, - HistogramBucketAggDependencies, -} from './histogram'; +import { IBucketHistogramAggConfig, getHistogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './bucket_agg_type'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { InternalStartServices } from '../../../types'; describe('Histogram Agg', () => { - let aggTypesDependencies: HistogramBucketAggDependencies; + let aggTypesDependencies: AggTypesDependencies; beforeEach(() => { - const { uiSettings } = coreMock.createSetup(); - - aggTypesDependencies = { - uiSettings, - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; + aggTypesDependencies = { ...mockAggTypesDependencies }; }); const getAggConfigs = (params: Record) => { @@ -72,7 +57,7 @@ describe('Histogram Agg', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getHistogramBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry(aggTypesDependencies), } ); }; @@ -167,10 +152,7 @@ describe('Histogram Agg', () => { ) => { aggTypesDependencies = { ...aggTypesDependencies, - uiSettings: { - ...aggTypesDependencies.uiSettings, - get: () => maxBars as any, - }, + getConfig: () => maxBars as any, }; const aggConfigs = getAggConfigs({ diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts similarity index 93% rename from src/plugins/data/public/search/aggs/buckets/histogram.ts rename to src/plugins/data/common/search/aggs/buckets/histogram.ts index 500b6eab75d771..2b263013e55a2a 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -19,14 +19,14 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { IUiSettingsClient } from 'src/core/public'; + +import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; +import { AggTypesDependencies } from '../agg_types'; +import { BaseAggParams } from '../types'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; -import { BaseAggParams } from '../types'; import { ExtendedBounds } from './lib/extended_bounds'; export interface AutoBounds { @@ -35,8 +35,8 @@ export interface AutoBounds { } export interface HistogramBucketAggDependencies { - uiSettings: IUiSettingsClient; - getInternalStartServices: GetInternalStartServicesFn; + getConfig: (key: string) => T; + getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart']; } export interface IBucketHistogramAggConfig extends IBucketAggConfig { @@ -54,8 +54,8 @@ export interface AggParamsHistogram extends BaseAggParams { } export const getHistogramBucketAgg = ({ - uiSettings, - getInternalStartServices, + getConfig, + getFieldFormatsStart, }: HistogramBucketAggDependencies) => new BucketAggType({ name: BUCKET_TYPES.HISTOGRAM, @@ -66,7 +66,7 @@ export const getHistogramBucketAgg = ({ makeLabel(aggConfig) { return aggConfig.getFieldDisplayName(); }, - createFilter: createFilterHistogram(getInternalStartServices), + createFilter: createFilterHistogram(getFieldFormatsStart), decorateAggConfig() { let autoBounds: AutoBounds; @@ -154,8 +154,8 @@ export const getHistogramBucketAgg = ({ const range = autoBounds.max - autoBounds.min; const bars = range / interval; - if (bars > uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { - const minInterval = range / uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); + if (bars > getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { + const minInterval = range / getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS); // Round interval by order of magnitude to provide clean intervals // Always round interval up so there will always be less buckets than histogram:maxBars diff --git a/src/plugins/data/public/search/aggs/buckets/histogram_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/histogram_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/histogram_fn.ts rename to src/plugins/data/common/search/aggs/buckets/histogram_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/index.ts similarity index 97% rename from src/plugins/data/public/search/aggs/buckets/index.ts rename to src/plugins/data/common/search/aggs/buckets/index.ts index 7036cc7785db7f..b16242e5198721 100644 --- a/src/plugins/data/public/search/aggs/buckets/index.ts +++ b/src/plugins/data/common/search/aggs/buckets/index.ts @@ -18,6 +18,7 @@ */ export * from './_interval_options'; +export * from './bucket_agg_type'; export * from './bucket_agg_types'; export * from './histogram'; export * from './date_histogram'; diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range.ts b/src/plugins/data/common/search/aggs/buckets/ip_range.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/ip_range.ts rename to src/plugins/data/common/search/aggs/buckets/ip_range.ts diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/ip_range_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/ip_range_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts rename to src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.test.ts rename to src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.ts b/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts similarity index 97% rename from src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.ts rename to src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts index 57a7b378f305f5..e4f6ab1e8da3cb 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/cidr_mask.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Ipv4Address } from '../../../../../common'; +import { Ipv4Address } from '../../utils'; const NUM_BITS = 32; diff --git a/src/plugins/data/public/search/aggs/buckets/lib/date_range.ts b/src/plugins/data/common/search/aggs/buckets/lib/date_range.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/date_range.ts rename to src/plugins/data/common/search/aggs/buckets/lib/date_range.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/extended_bounds.ts b/src/plugins/data/common/search/aggs/buckets/lib/extended_bounds.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/extended_bounds.ts rename to src/plugins/data/common/search/aggs/buckets/lib/extended_bounds.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/geo_point.ts b/src/plugins/data/common/search/aggs/buckets/lib/geo_point.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/geo_point.ts rename to src/plugins/data/common/search/aggs/buckets/lib/geo_point.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/ip_range.ts b/src/plugins/data/common/search/aggs/buckets/lib/ip_range.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/ip_range.ts rename to src/plugins/data/common/search/aggs/buckets/lib/ip_range.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.test.ts rename to src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts rename to src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_auto_interval.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts similarity index 97% rename from src/plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts rename to src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts index 3e7d315a0a42a2..0ef2c571ca8faf 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/calc_es_interval.ts @@ -20,7 +20,7 @@ import moment from 'moment'; import dateMath, { Unit } from '@elastic/datemath'; -import { parseEsInterval } from '../../../../../../common'; +import { parseEsInterval } from '../../../utils'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('M'); diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/index.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/time_buckets/index.ts rename to src/plugins/data/common/search/aggs/buckets/lib/time_buckets/index.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts rename to src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts similarity index 99% rename from src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts rename to src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts index 017f646258c012..6402a6e83ead9a 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -20,7 +20,7 @@ import { isString, isObject as isObjectLodash, isPlainObject, sortBy } from 'lodash'; import moment, { Moment } from 'moment'; -import { parseInterval } from '../../../../../../common'; +import { parseInterval } from '../../../utils'; import { TimeRangeBounds } from '../../../../../query'; import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; import { diff --git a/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts b/src/plugins/data/common/search/aggs/buckets/migrate_include_exclude_format.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts rename to src/plugins/data/common/search/aggs/buckets/migrate_include_exclude_format.ts diff --git a/src/plugins/data/public/search/aggs/buckets/range.test.ts b/src/plugins/data/common/search/aggs/buckets/range.test.ts similarity index 79% rename from src/plugins/data/public/search/aggs/buckets/range.test.ts rename to src/plugins/data/common/search/aggs/buckets/range.test.ts index f7c61a638158ce..b23b03db6a9ec2 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.test.ts @@ -17,26 +17,12 @@ * under the License. */ -import { getRangeBucketAgg, RangeBucketAggDependencies } from './range'; import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { FieldFormatsGetConfigFn, NumberFormat } from '../../../../common'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { InternalStartServices } from '../../../types'; +import { FieldFormatsGetConfigFn, NumberFormat } from '../../../../common/field_formats'; describe('Range Agg', () => { - let aggTypesDependencies: RangeBucketAggDependencies; - - beforeEach(() => { - aggTypesDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - }); - const getConfig = (() => {}) as FieldFormatsGetConfigFn; const getAggConfigs = () => { const field = { @@ -74,7 +60,7 @@ describe('Range Agg', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry(), } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/range.ts b/src/plugins/data/common/search/aggs/buckets/range.ts similarity index 90% rename from src/plugins/data/public/search/aggs/buckets/range.ts rename to src/plugins/data/common/search/aggs/buckets/range.ts index 9f54f9fd0704e8..91a357b6359504 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.ts @@ -18,20 +18,22 @@ */ import { i18n } from '@kbn/i18n'; -import { BucketAggType } from './bucket_agg_type'; + import { KBN_FIELD_TYPES } from '../../../../common'; +import { AggTypesDependencies } from '../agg_types'; +import { BaseAggParams } from '../types'; + +import { BucketAggType } from './bucket_agg_type'; import { RangeKey } from './range_key'; import { createFilterRange } from './create_filter/range'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { GetInternalStartServicesFn } from '../../../types'; -import { BaseAggParams } from '../types'; const rangeTitle = i18n.translate('data.search.aggs.buckets.rangeTitle', { defaultMessage: 'Range', }); export interface RangeBucketAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; + getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart']; } export interface AggParamsRange extends BaseAggParams { @@ -42,13 +44,13 @@ export interface AggParamsRange extends BaseAggParams { }>; } -export const getRangeBucketAgg = ({ getInternalStartServices }: RangeBucketAggDependencies) => { +export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDependencies) => { const keyCaches = new WeakMap(); return new BucketAggType({ name: BUCKET_TYPES.RANGE, title: rangeTitle, - createFilter: createFilterRange(getInternalStartServices), + createFilter: createFilterRange(getFieldFormatsStart), makeLabel(aggConfig) { return i18n.translate('data.search.aggs.aggTypesLabel', { defaultMessage: '{fieldName} ranges', diff --git a/src/plugins/data/public/search/aggs/buckets/range_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/range_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/range_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/range_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/range_fn.ts b/src/plugins/data/common/search/aggs/buckets/range_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/range_fn.ts rename to src/plugins/data/common/search/aggs/buckets/range_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/range_key.ts b/src/plugins/data/common/search/aggs/buckets/range_key.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/range_key.ts rename to src/plugins/data/common/search/aggs/buckets/range_key.ts diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts similarity index 95% rename from src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts rename to src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts index f13fafc2b17e6e..e6c7bbee72a722 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts @@ -20,7 +20,6 @@ import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { getSignificantTermsBucketAgg } from './significant_terms'; describe('Significant Terms Agg', () => { describe('order agg editor UI', () => { @@ -51,7 +50,7 @@ describe('Significant Terms Agg', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getSignificantTermsBucketAgg()]), + typesRegistry: mockAggTypesRegistry(), } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/significant_terms.ts rename to src/plugins/data/common/search/aggs/buckets/significant_terms.ts diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/significant_terms_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/significant_terms_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts rename to src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts diff --git a/src/plugins/data/public/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/terms.test.ts rename to src/plugins/data/common/search/aggs/buckets/terms.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/terms.ts rename to src/plugins/data/common/search/aggs/buckets/terms.ts diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/terms_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts rename to src/plugins/data/common/search/aggs/buckets/terms_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/common/search/aggs/buckets/terms_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/buckets/terms_fn.ts rename to src/plugins/data/common/search/aggs/buckets/terms_fn.ts diff --git a/src/plugins/data/public/search/aggs/index.test.ts b/src/plugins/data/common/search/aggs/index.test.ts similarity index 63% rename from src/plugins/data/public/search/aggs/index.test.ts rename to src/plugins/data/common/search/aggs/index.test.ts index 73068326ca97ee..1fdc28d1c7839f 100644 --- a/src/plugins/data/public/search/aggs/index.test.ts +++ b/src/plugins/data/common/search/aggs/index.test.ts @@ -17,42 +17,34 @@ * under the License. */ -import { coreMock } from '../../../../../../src/core/public/mocks'; import { getAggTypes } from './index'; +import { mockGetFieldFormatsStart } from './test_helpers'; import { isBucketAggType } from './buckets/bucket_agg_type'; import { isMetricAggType } from './metrics/metric_agg_type'; -import { FieldFormatsStart } from '../../field_formats'; -import { InternalStartServices } from '../../types'; describe('AggTypesComponent', () => { - const coreSetup = coreMock.createSetup(); - const coreStart = coreMock.createSetup(); - - const aggTypes = getAggTypes({ - calculateBounds: jest.fn(), - getInternalStartServices: () => - (({ - notifications: coreStart.notifications, - fieldFormats: {} as FieldFormatsStart, - } as unknown) as InternalStartServices), - uiSettings: coreSetup.uiSettings, - }); - + const aggTypes = getAggTypes(); const { buckets, metrics } = aggTypes; + const aggTypesDependencies = { + calculateBounds: jest.fn(), + getConfig: jest.fn(), + getFieldFormatsStart: mockGetFieldFormatsStart, + isDefaultTimezone: jest.fn().mockReturnValue(true), + }; describe('bucket aggs', () => { test('all extend BucketAggType', () => { - buckets.forEach((bucketAgg) => { - expect(isBucketAggType(bucketAgg)).toBeTruthy(); + buckets.forEach(({ fn }) => { + expect(isBucketAggType(fn(aggTypesDependencies))).toBeTruthy(); }); }); }); describe('metric aggs', () => { test('all extend MetricAggType', () => { - metrics.forEach((metricAgg) => { - expect(isMetricAggType(metricAgg)).toBeTruthy(); + metrics.forEach(({ fn }) => { + expect(isMetricAggType(fn(aggTypesDependencies))).toBeTruthy(); }); }); }); diff --git a/src/plugins/data/common/search/aggs/index.ts b/src/plugins/data/common/search/aggs/index.ts index 7a5b7d509c9403..1c4ae27e495305 100644 --- a/src/plugins/data/common/search/aggs/index.ts +++ b/src/plugins/data/common/search/aggs/index.ts @@ -17,5 +17,15 @@ * under the License. */ -export * from './date_interval_utils'; -export * from './ipv4_address'; +export * from './agg_config'; +export * from './agg_configs'; +export * from './agg_groups'; +export * from './agg_type'; +export * from './agg_types'; +export * from './agg_types_registry'; +export * from './aggs_service'; +export * from './buckets'; +export * from './metrics'; +export * from './param_types'; +export * from './types'; +export * from './utils'; diff --git a/src/plugins/data/public/search/aggs/metrics/avg.ts b/src/plugins/data/common/search/aggs/metrics/avg.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/avg.ts rename to src/plugins/data/common/search/aggs/metrics/avg.ts diff --git a/src/plugins/data/public/search/aggs/metrics/avg_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/avg_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/avg_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/avg_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/avg_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/avg_fn.ts rename to src/plugins/data/common/search/aggs/metrics/avg_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts b/src/plugins/data/common/search/aggs/metrics/bucket_avg.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_avg.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_avg.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max.ts b/src/plugins/data/common/search/aggs/metrics/bucket_max.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_max.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_max.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_max_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_max_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min.ts b/src/plugins/data/common/search/aggs/metrics/bucket_min.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_min.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_min.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_min_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_min_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts b/src/plugins/data/common/search/aggs/metrics/bucket_sum.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_sum.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_sum.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts rename to src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality.ts b/src/plugins/data/common/search/aggs/metrics/cardinality.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/cardinality.ts rename to src/plugins/data/common/search/aggs/metrics/cardinality.ts diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/cardinality_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts rename to src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/count.ts rename to src/plugins/data/common/search/aggs/metrics/count.ts diff --git a/src/plugins/data/public/search/aggs/metrics/count_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/count_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/count_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/count_fn.ts rename to src/plugins/data/common/search/aggs/metrics/count_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts b/src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts rename to src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts rename to src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/derivative.ts b/src/plugins/data/common/search/aggs/metrics/derivative.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/derivative.ts rename to src/plugins/data/common/search/aggs/metrics/derivative.ts diff --git a/src/plugins/data/public/search/aggs/metrics/derivative_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/derivative_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/derivative_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/derivative_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts b/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/derivative_fn.ts rename to src/plugins/data/common/search/aggs/metrics/derivative_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts b/src/plugins/data/common/search/aggs/metrics/geo_bounds.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/geo_bounds.ts rename to src/plugins/data/common/search/aggs/metrics/geo_bounds.ts diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts rename to src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts b/src/plugins/data/common/search/aggs/metrics/geo_centroid.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/geo_centroid.ts rename to src/plugins/data/common/search/aggs/metrics/geo_centroid.ts diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts rename to src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/index.ts rename to src/plugins/data/common/search/aggs/metrics/index.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts b/src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/get_response_agg_config_class.ts rename to src/plugins/data/common/search/aggs/metrics/lib/get_response_agg_config_class.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/make_nested_label.test.ts b/src/plugins/data/common/search/aggs/metrics/lib/make_nested_label.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/make_nested_label.test.ts rename to src/plugins/data/common/search/aggs/metrics/lib/make_nested_label.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/make_nested_label.ts b/src/plugins/data/common/search/aggs/metrics/lib/make_nested_label.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/make_nested_label.ts rename to src/plugins/data/common/search/aggs/metrics/lib/make_nested_label.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/nested_agg_helpers.ts b/src/plugins/data/common/search/aggs/metrics/lib/nested_agg_helpers.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/nested_agg_helpers.ts rename to src/plugins/data/common/search/aggs/metrics/lib/nested_agg_helpers.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/ordinal_suffix.test.ts b/src/plugins/data/common/search/aggs/metrics/lib/ordinal_suffix.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/ordinal_suffix.test.ts rename to src/plugins/data/common/search/aggs/metrics/lib/ordinal_suffix.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/ordinal_suffix.ts b/src/plugins/data/common/search/aggs/metrics/lib/ordinal_suffix.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/ordinal_suffix.ts rename to src/plugins/data/common/search/aggs/metrics/lib/ordinal_suffix.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts rename to src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_writer.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_writer.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_writer.ts rename to src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_writer.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts rename to src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_writer.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_writer.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_writer.ts rename to src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_writer.ts diff --git a/src/plugins/data/public/search/aggs/metrics/max.ts b/src/plugins/data/common/search/aggs/metrics/max.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/max.ts rename to src/plugins/data/common/search/aggs/metrics/max.ts diff --git a/src/plugins/data/public/search/aggs/metrics/max_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/max_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/max_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/max_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/max_fn.ts b/src/plugins/data/common/search/aggs/metrics/max_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/max_fn.ts rename to src/plugins/data/common/search/aggs/metrics/max_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/median.test.ts b/src/plugins/data/common/search/aggs/metrics/median.test.ts similarity index 94% rename from src/plugins/data/public/search/aggs/metrics/median.test.ts rename to src/plugins/data/common/search/aggs/metrics/median.test.ts index 22d907330e2a32..f3f2d157ebafc4 100644 --- a/src/plugins/data/public/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { getMedianMetricAgg } from './median'; import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; @@ -26,7 +25,7 @@ describe('AggTypeMetricMedianProvider class', () => { let aggConfigs: IAggConfigs; beforeEach(() => { - const typesRegistry = mockAggTypesRegistry([getMedianMetricAgg()]); + const typesRegistry = mockAggTypesRegistry(); const field = { name: 'bytes', }; diff --git a/src/plugins/data/public/search/aggs/metrics/median.ts b/src/plugins/data/common/search/aggs/metrics/median.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/median.ts rename to src/plugins/data/common/search/aggs/metrics/median.ts diff --git a/src/plugins/data/public/search/aggs/metrics/median_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/median_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/median_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/median_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/median_fn.ts b/src/plugins/data/common/search/aggs/metrics/median_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/median_fn.ts rename to src/plugins/data/common/search/aggs/metrics/median_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/metric_agg_type.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/metric_agg_type.ts rename to src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts diff --git a/src/plugins/data/public/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/metric_agg_types.ts rename to src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts diff --git a/src/plugins/data/public/search/aggs/metrics/min.ts b/src/plugins/data/common/search/aggs/metrics/min.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/min.ts rename to src/plugins/data/common/search/aggs/metrics/min.ts diff --git a/src/plugins/data/public/search/aggs/metrics/min_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/min_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/min_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/min_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/min_fn.ts b/src/plugins/data/common/search/aggs/metrics/min_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/min_fn.ts rename to src/plugins/data/common/search/aggs/metrics/min_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg.ts b/src/plugins/data/common/search/aggs/metrics/moving_avg.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/moving_avg.ts rename to src/plugins/data/common/search/aggs/metrics/moving_avg.ts diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/moving_avg_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/moving_avg_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts rename to src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts rename to src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts similarity index 77% rename from src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts rename to src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts index 348aecc23243ae..970daf5b624585 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts @@ -23,29 +23,16 @@ import { PercentileRanksMetricAggDependencies, } from './percentile_ranks'; import { AggConfigs, IAggConfigs } from '../agg_configs'; -import { mockAggTypesRegistry } from '../test_helpers'; +import { mockAggTypesRegistry, mockGetFieldFormatsStart } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -import { FieldFormatsStart } from '../../../field_formats'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentileRanksProvider class', function () { let aggConfigs: IAggConfigs; - let fieldFormats: FieldFormatsStart; let aggTypesDependencies: PercentileRanksMetricAggDependencies; beforeEach(() => { - fieldFormats = fieldFormatsServiceMock.createStartContract(); - fieldFormats.getDefaultInstance = (() => ({ - convert: (t?: string) => t, - })) as any; - aggTypesDependencies = { - getInternalStartServices: () => - (({ - fieldFormats, - } as unknown) as InternalStartServices), - }; - const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]); + aggTypesDependencies = { getFieldFormatsStart: mockGetFieldFormatsStart }; + const typesRegistry = mockAggTypesRegistry(); const field = { name: 'bytes', }; diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.ts similarity index 86% rename from src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts rename to src/plugins/data/common/search/aggs/metrics/percentile_ranks.ts index 3c0be229f1bbdc..664cc1ad02adae 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.ts @@ -18,13 +18,15 @@ */ import { i18n } from '@kbn/i18n'; + +import { KBN_FIELD_TYPES } from '../../../../common'; +import { AggTypesDependencies } from '../agg_types'; +import { BaseAggParams } from '../types'; + import { MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { getPercentileValue } from './percentiles_get_value'; import { METRIC_TYPES } from './metric_agg_types'; -import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; -import { BaseAggParams } from '../types'; export interface AggParamsPercentileRanks extends BaseAggParams { field: string; @@ -35,16 +37,17 @@ export interface AggParamsPercentileRanks extends BaseAggParams { export type IPercentileRanksAggConfig = IResponseAggConfig; export interface PercentileRanksMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; + getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart']; } -const getValueProps = (getInternalStartServices: GetInternalStartServicesFn) => { +const getValueProps = ( + getFieldFormatsStart: PercentileRanksMetricAggDependencies['getFieldFormatsStart'] +) => { return { makeLabel(this: IPercentileRanksAggConfig) { - const { fieldFormats } = getInternalStartServices(); + const { getDefaultInstance } = getFieldFormatsStart(); const field = this.getField(); - const format = - (field && field.format) || fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); + const format = (field && field.format) || getDefaultInstance(KBN_FIELD_TYPES.NUMBER); const customLabel = this.getParam('customLabel'); const label = customLabel || this.getFieldDisplayName(); @@ -57,7 +60,7 @@ const getValueProps = (getInternalStartServices: GetInternalStartServicesFn) => }; export const getPercentileRanksMetricAgg = ({ - getInternalStartServices, + getFieldFormatsStart, }: PercentileRanksMetricAggDependencies) => { return new MetricAggType({ name: METRIC_TYPES.PERCENTILE_RANKS, @@ -87,10 +90,7 @@ export const getPercentileRanksMetricAgg = ({ }, ], getResponseAggs(agg) { - const ValueAggConfig = getResponseAggConfigClass( - agg, - getValueProps(getInternalStartServices) - ); + const ValueAggConfig = getResponseAggConfigClass(agg, getValueProps(getFieldFormatsStart)); const values = agg.getParam('values'); return values.map((value: any) => new ValueAggConfig(value)); diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts rename to src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts similarity index 96% rename from src/plugins/data/public/search/aggs/metrics/percentiles.test.ts rename to src/plugins/data/common/search/aggs/metrics/percentiles.test.ts index a44c0e5075ef95..10e98df5a4eeb1 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts @@ -26,7 +26,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { let aggConfigs: IAggConfigs; beforeEach(() => { - const typesRegistry = mockAggTypesRegistry([getPercentilesMetricAgg()]); + const typesRegistry = mockAggTypesRegistry(); const field = { name: 'bytes', }; diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.ts b/src/plugins/data/common/search/aggs/metrics/percentiles.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/percentiles.ts rename to src/plugins/data/common/search/aggs/metrics/percentiles.ts diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/percentiles_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/percentiles_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts rename to src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles_get_value.ts b/src/plugins/data/common/search/aggs/metrics/percentiles_get_value.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/percentiles_get_value.ts rename to src/plugins/data/common/search/aggs/metrics/percentiles_get_value.ts diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff.ts b/src/plugins/data/common/search/aggs/metrics/serial_diff.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/serial_diff.ts rename to src/plugins/data/common/search/aggs/metrics/serial_diff.ts diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/serial_diff_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/serial_diff_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts rename to src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts rename to src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts similarity index 97% rename from src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts rename to src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts index c3efe95f44a56d..f2f30fcde42eb7 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts @@ -23,7 +23,7 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; describe('AggTypeMetricStandardDeviationProvider class', () => { - const typesRegistry = mockAggTypesRegistry([getStdDeviationMetricAgg()]); + const typesRegistry = mockAggTypesRegistry(); const getAggConfigs = (customLabel?: string) => { const field = { name: 'memory', diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/std_deviation.ts rename to src/plugins/data/common/search/aggs/metrics/std_deviation.ts diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/std_deviation_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/std_deviation_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts rename to src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/sum.ts b/src/plugins/data/common/search/aggs/metrics/sum.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/sum.ts rename to src/plugins/data/common/search/aggs/metrics/sum.ts diff --git a/src/plugins/data/public/search/aggs/metrics/sum_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/sum_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/sum_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/sum_fn.ts rename to src/plugins/data/common/search/aggs/metrics/sum_fn.ts diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts similarity index 99% rename from src/plugins/data/public/search/aggs/metrics/top_hit.test.ts rename to src/plugins/data/common/search/aggs/metrics/top_hit.test.ts index c2434df3ae53c1..c0cbfb33c842b5 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts @@ -36,7 +36,7 @@ describe('Top hit metric', () => { fieldType = KBN_FIELD_TYPES.NUMBER, size = 1, }: any) => { - const typesRegistry = mockAggTypesRegistry([getTopHitMetricAgg()]); + const typesRegistry = mockAggTypesRegistry(); const field = { name: fieldName, displayName: fieldName, diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/top_hit.ts rename to src/plugins/data/common/search/aggs/metrics/top_hit.ts diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/top_hit_fn.test.ts rename to src/plugins/data/common/search/aggs/metrics/top_hit_fn.test.ts diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts similarity index 100% rename from src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts rename to src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts diff --git a/src/plugins/data/public/search/aggs/param_types/agg.ts b/src/plugins/data/common/search/aggs/param_types/agg.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/agg.ts rename to src/plugins/data/common/search/aggs/param_types/agg.ts diff --git a/src/plugins/data/public/search/aggs/param_types/base.ts b/src/plugins/data/common/search/aggs/param_types/base.ts similarity index 96% rename from src/plugins/data/public/search/aggs/param_types/base.ts rename to src/plugins/data/common/search/aggs/param_types/base.ts index 1ba8a75e98cbe9..3a12a9a54500f4 100644 --- a/src/plugins/data/public/search/aggs/param_types/base.ts +++ b/src/plugins/data/common/search/aggs/param_types/base.ts @@ -17,11 +17,10 @@ * under the License. */ +import { FetchOptions, ISearchSource } from 'src/plugins/data/public'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { IAggConfigs } from '../agg_configs'; import { IAggConfig } from '../agg_config'; -import { FetchOptions } from '../../fetch'; -import { ISearchSource } from '../../search_source'; export class BaseParamType { name: string; diff --git a/src/plugins/data/public/search/aggs/param_types/field.test.ts b/src/plugins/data/common/search/aggs/param_types/field.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/field.test.ts rename to src/plugins/data/common/search/aggs/param_types/field.test.ts diff --git a/src/plugins/data/public/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts similarity index 96% rename from src/plugins/data/public/search/aggs/param_types/field.ts rename to src/plugins/data/common/search/aggs/param_types/field.ts index 7c00bc668a39f6..492294bdf4e5f8 100644 --- a/src/plugins/data/public/search/aggs/param_types/field.ts +++ b/src/plugins/data/common/search/aggs/param_types/field.ts @@ -22,8 +22,8 @@ import { IAggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/common'; import { BaseParamType } from './base'; import { propFilter } from '../utils'; -import { isNestedField, KBN_FIELD_TYPES } from '../../../../common'; -import { IndexPatternField } from '../../../index_patterns'; +import { KBN_FIELD_TYPES } from '../../../kbn_field_types/types'; +import { isNestedField, IndexPatternField } from '../../../index_patterns/fields'; const filterByType = propFilter('type'); diff --git a/src/plugins/data/public/search/aggs/param_types/index.ts b/src/plugins/data/common/search/aggs/param_types/index.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/index.ts rename to src/plugins/data/common/search/aggs/param_types/index.ts diff --git a/src/plugins/data/public/search/aggs/param_types/json.test.ts b/src/plugins/data/common/search/aggs/param_types/json.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/json.test.ts rename to src/plugins/data/common/search/aggs/param_types/json.test.ts diff --git a/src/plugins/data/public/search/aggs/param_types/json.ts b/src/plugins/data/common/search/aggs/param_types/json.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/json.ts rename to src/plugins/data/common/search/aggs/param_types/json.ts diff --git a/src/plugins/data/public/search/aggs/param_types/optioned.test.ts b/src/plugins/data/common/search/aggs/param_types/optioned.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/optioned.test.ts rename to src/plugins/data/common/search/aggs/param_types/optioned.test.ts diff --git a/src/plugins/data/public/search/aggs/param_types/optioned.ts b/src/plugins/data/common/search/aggs/param_types/optioned.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/optioned.ts rename to src/plugins/data/common/search/aggs/param_types/optioned.ts diff --git a/src/plugins/data/public/search/aggs/param_types/string.test.ts b/src/plugins/data/common/search/aggs/param_types/string.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/string.test.ts rename to src/plugins/data/common/search/aggs/param_types/string.test.ts diff --git a/src/plugins/data/public/search/aggs/param_types/string.ts b/src/plugins/data/common/search/aggs/param_types/string.ts similarity index 100% rename from src/plugins/data/public/search/aggs/param_types/string.ts rename to src/plugins/data/common/search/aggs/param_types/string.ts diff --git a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts b/src/plugins/data/common/search/aggs/test_helpers/function_wrapper.ts similarity index 100% rename from src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts rename to src/plugins/data/common/search/aggs/test_helpers/function_wrapper.ts diff --git a/src/plugins/data/public/search/aggs/test_helpers/index.ts b/src/plugins/data/common/search/aggs/test_helpers/index.ts similarity index 86% rename from src/plugins/data/public/search/aggs/test_helpers/index.ts rename to src/plugins/data/common/search/aggs/test_helpers/index.ts index d47317d8b4725f..30f315f2767412 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/index.ts +++ b/src/plugins/data/common/search/aggs/test_helpers/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { functionWrapper } from './function_wrapper'; -export { mockAggTypesRegistry } from './mock_agg_types_registry'; +export * from './function_wrapper'; +export * from './mock_agg_types_registry'; diff --git a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts similarity index 52% rename from src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts rename to src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts index 4a0820c349b5f5..14631a9d530558 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts +++ b/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts @@ -17,17 +17,14 @@ * under the License. */ -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { fieldFormatsMock } from '../../../field_formats/mocks'; + import { AggTypesRegistry, AggTypesRegistryStart } from '../agg_types_registry'; -import { getAggTypes } from '../agg_types'; -import { BucketAggType } from '../buckets/bucket_agg_type'; -import { MetricAggType } from '../metrics/metric_agg_type'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { InternalStartServices } from '../../../types'; +import { AggTypesDependencies, getAggTypes } from '../agg_types'; import { TimeBucketsConfig } from '../buckets/lib/time_buckets/time_buckets'; // Mocked uiSettings shared among aggs unit tests -const mockUiSettings = jest.fn().mockImplementation((key: string) => { +const mockGetConfig = jest.fn().mockImplementation((key: string) => { const config: TimeBucketsConfig = { 'histogram:maxBars': 4, 'histogram:barTarget': 3, @@ -44,6 +41,23 @@ const mockUiSettings = jest.fn().mockImplementation((key: string) => { return config[key] ?? key; }); +/** @internal */ +export function mockGetFieldFormatsStart() { + const { deserialize, getDefaultInstance } = fieldFormatsMock; + return { + deserialize, + getDefaultInstance, + }; +} + +/** @internal */ +export const mockAggTypesDependencies: AggTypesDependencies = { + calculateBounds: jest.fn(), + getFieldFormatsStart: mockGetFieldFormatsStart, + getConfig: mockGetConfig, + isDefaultTimezone: () => true, +}; + /** * Testing utility which creates a new instance of AggTypesRegistry, * registers the provided agg types, and returns AggTypesRegistry.start() @@ -56,36 +70,37 @@ const mockUiSettings = jest.fn().mockImplementation((key: string) => { * * @internal */ -export function mockAggTypesRegistry | MetricAggType>( - types?: T[] -): AggTypesRegistryStart { +export function mockAggTypesRegistry(deps?: AggTypesDependencies): AggTypesRegistryStart { const registry = new AggTypesRegistry(); + const initializedAggTypes = new Map(); const registrySetup = registry.setup(); - if (types) { - types.forEach((type) => { - if (type instanceof BucketAggType) { - registrySetup.registerBucket(type); - } else if (type instanceof MetricAggType) { - registrySetup.registerMetric(type); - } - }); - } else { - const coreSetup = coreMock.createSetup(); - coreSetup.uiSettings.get = mockUiSettings; + const aggTypes = getAggTypes(); + + aggTypes.buckets.forEach(({ name, fn }) => registrySetup.registerBucket(name, fn)); + aggTypes.metrics.forEach(({ name, fn }) => registrySetup.registerMetric(name, fn)); - const aggTypes = getAggTypes({ - calculateBounds: jest.fn(), - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - uiSettings: coreSetup.uiSettings, - }); + const registryStart = registry.start(); - aggTypes.buckets.forEach((type) => registrySetup.registerBucket(type)); - aggTypes.metrics.forEach((type) => registrySetup.registerMetric(type)); - } + // initialize each agg type and store in memory + registryStart.getAll().buckets.forEach((type) => { + const agg = type(deps ?? mockAggTypesDependencies); + initializedAggTypes.set(agg.name, agg); + }); + registryStart.getAll().metrics.forEach((type) => { + const agg = type(deps ?? mockAggTypesDependencies); + initializedAggTypes.set(agg.name, agg); + }); - return registry.start(); + return { + get: (name: string) => { + return initializedAggTypes.get(name); + }, + getAll: () => { + return { + buckets: Array.from(initializedAggTypes.values()).filter((agg) => agg.type === 'buckets'), + metrics: Array.from(initializedAggTypes.values()).filter((agg) => agg.type === 'metrics'), + }; + }, + }; } diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts new file mode 100644 index 00000000000000..dabd653463d4fd --- /dev/null +++ b/src/plugins/data/common/search/aggs/types.ts @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Assign } from '@kbn/utility-types'; +import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; +import { + AggConfigSerialized, + AggConfigs, + AggParamsRange, + AggParamsIpRange, + AggParamsDateRange, + AggParamsFilter, + AggParamsFilters, + AggParamsSignificantTerms, + AggParamsGeoTile, + AggParamsGeoHash, + AggParamsTerms, + AggParamsAvg, + AggParamsCardinality, + AggParamsGeoBounds, + AggParamsGeoCentroid, + AggParamsMax, + AggParamsMedian, + AggParamsMin, + AggParamsStdDeviation, + AggParamsSum, + AggParamsBucketAvg, + AggParamsBucketMax, + AggParamsBucketMin, + AggParamsBucketSum, + AggParamsCumulativeSum, + AggParamsDerivative, + AggParamsMovingAvg, + AggParamsPercentileRanks, + AggParamsPercentiles, + AggParamsSerialDiff, + AggParamsTopHit, + AggParamsHistogram, + AggParamsDateHistogram, + AggTypesRegistry, + AggTypesRegistrySetup, + AggTypesRegistryStart, + CreateAggConfigParams, + getCalculateAutoTimeExpression, + METRIC_TYPES, + BUCKET_TYPES, +} from './'; + +export { IAggConfig, AggConfigSerialized } from './agg_config'; +export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; +export { IAggType } from './agg_type'; +export { AggParam, AggParamOption } from './agg_params'; +export { IFieldParamType } from './param_types'; +export { IMetricAggType } from './metrics/metric_agg_type'; +export { DateRangeKey } from './buckets/lib/date_range'; +export { IpRangeKey } from './buckets/lib/ip_range'; +export { OptionedValueProp } from './param_types/optioned'; + +/** @internal */ +export interface AggsCommonSetup { + types: AggTypesRegistrySetup; +} + +/** @internal */ +export interface AggsCommonStart { + calculateAutoTimeExpression: ReturnType; + createAggConfigs: ( + indexPattern: IndexPattern, + configStates?: CreateAggConfigParams[], + schemas?: Record + ) => InstanceType; + types: ReturnType; +} + +/** + * AggsStart represents the actual external contract as AggsCommonStart + * is only used internally. The difference is that AggsStart includes the + * typings for the registry with initialized agg types. + * + * @internal + */ +export type AggsStart = Assign; + +/** @internal */ +export interface BaseAggParams { + json?: string; + customLabel?: string; +} + +/** @internal */ +export interface AggExpressionType { + type: 'agg_type'; + value: AggConfigSerialized; +} + +/** @internal */ +export type AggExpressionFunctionArgs< + Name extends keyof AggParamsMapping +> = AggParamsMapping[Name] & Pick; + +/** + * A global list of the param interfaces for each agg type. + * For now this is internal, but eventually we will probably + * want to make it public. + * + * @internal + */ +export interface AggParamsMapping { + [BUCKET_TYPES.RANGE]: AggParamsRange; + [BUCKET_TYPES.IP_RANGE]: AggParamsIpRange; + [BUCKET_TYPES.DATE_RANGE]: AggParamsDateRange; + [BUCKET_TYPES.FILTER]: AggParamsFilter; + [BUCKET_TYPES.FILTERS]: AggParamsFilters; + [BUCKET_TYPES.SIGNIFICANT_TERMS]: AggParamsSignificantTerms; + [BUCKET_TYPES.GEOTILE_GRID]: AggParamsGeoTile; + [BUCKET_TYPES.GEOHASH_GRID]: AggParamsGeoHash; + [BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram; + [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; + [BUCKET_TYPES.TERMS]: AggParamsTerms; + [METRIC_TYPES.AVG]: AggParamsAvg; + [METRIC_TYPES.CARDINALITY]: AggParamsCardinality; + [METRIC_TYPES.COUNT]: BaseAggParams; + [METRIC_TYPES.GEO_BOUNDS]: AggParamsGeoBounds; + [METRIC_TYPES.GEO_CENTROID]: AggParamsGeoCentroid; + [METRIC_TYPES.MAX]: AggParamsMax; + [METRIC_TYPES.MEDIAN]: AggParamsMedian; + [METRIC_TYPES.MIN]: AggParamsMin; + [METRIC_TYPES.STD_DEV]: AggParamsStdDeviation; + [METRIC_TYPES.SUM]: AggParamsSum; + [METRIC_TYPES.AVG_BUCKET]: AggParamsBucketAvg; + [METRIC_TYPES.MAX_BUCKET]: AggParamsBucketMax; + [METRIC_TYPES.MIN_BUCKET]: AggParamsBucketMin; + [METRIC_TYPES.SUM_BUCKET]: AggParamsBucketSum; + [METRIC_TYPES.CUMULATIVE_SUM]: AggParamsCumulativeSum; + [METRIC_TYPES.DERIVATIVE]: AggParamsDerivative; + [METRIC_TYPES.MOVING_FN]: AggParamsMovingAvg; + [METRIC_TYPES.PERCENTILE_RANKS]: AggParamsPercentileRanks; + [METRIC_TYPES.PERCENTILES]: AggParamsPercentiles; + [METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff; + [METRIC_TYPES.TOP_HITS]: AggParamsTopHit; +} diff --git a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts similarity index 71% rename from src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts rename to src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts index 30fcdd9d83a389..622e8101f34aba 100644 --- a/src/plugins/data/public/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts @@ -17,11 +17,12 @@ * under the License. */ import moment from 'moment'; -import { IUiSettingsClient } from 'src/core/public'; +import { UI_SETTINGS } from '../../../../common/constants'; +import { TimeRange } from '../../../../common/query'; import { TimeBuckets } from '../buckets/lib/time_buckets'; -import { toAbsoluteDates, TimeRange, UI_SETTINGS } from '../../../../common'; +import { toAbsoluteDates } from './date_interval_utils'; -export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { +export function getCalculateAutoTimeExpression(getConfig: (key: string) => any) { return function calculateAutoTimeExpression(range: TimeRange) { const dates = toAbsoluteDates(range); if (!dates) { @@ -29,10 +30,10 @@ export function getCalculateAutoTimeExpression(uiSettings: IUiSettingsClient) { } const buckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + 'histogram:maxBars': getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': getConfig(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + dateFormat: getConfig('dateFormat'), + 'dateFormat:scaled': getConfig('dateFormat:scaled'), }); buckets.setInterval('auto'); diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/date_histogram_interval.test.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/date_histogram_interval.test.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/date_histogram_interval.test.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/date_histogram_interval.test.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/date_histogram_interval.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/date_histogram_interval.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/date_histogram_interval.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/date_histogram_interval.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/index.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/index.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/index.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/index.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/invalid_es_calendar_interval_error.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/invalid_es_calendar_interval_error.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/invalid_es_calendar_interval_error.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/invalid_es_calendar_interval_error.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/invalid_es_interval_format_error.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/invalid_es_interval_format_error.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/invalid_es_interval_format_error.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/invalid_es_interval_format_error.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/is_valid_es_interval.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/is_valid_es_interval.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/is_valid_es_interval.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/is_valid_es_interval.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/is_valid_interval.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/is_valid_interval.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/is_valid_interval.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/is_valid_interval.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/least_common_interval.test.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_interval.test.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/least_common_interval.test.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_interval.test.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/least_common_interval.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_interval.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/least_common_interval.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_interval.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/least_common_multiple.test.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_multiple.test.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/least_common_multiple.test.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_multiple.test.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/least_common_multiple.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_multiple.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/least_common_multiple.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/least_common_multiple.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_es_interval.test.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.test.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/parse_es_interval.test.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.test.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_es_interval.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/parse_es_interval.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_interval.test.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_interval.test.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_interval.ts similarity index 100% rename from src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_interval.ts diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/to_absolute_dates.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/to_absolute_dates.ts similarity index 95% rename from src/plugins/data/common/search/aggs/date_interval_utils/to_absolute_dates.ts rename to src/plugins/data/common/search/aggs/utils/date_interval_utils/to_absolute_dates.ts index 98d752a72e28ae..99809e06df38f6 100644 --- a/src/plugins/data/common/search/aggs/date_interval_utils/to_absolute_dates.ts +++ b/src/plugins/data/common/search/aggs/utils/date_interval_utils/to_absolute_dates.ts @@ -18,7 +18,7 @@ */ import dateMath from '@elastic/datemath'; -import { TimeRange } from '../../../../common'; +import { TimeRange } from '../../../../../common'; export function toAbsoluteDates(range: TimeRange) { const fromDate = dateMath.parse(range.from); diff --git a/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.test.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts similarity index 95% rename from src/plugins/data/public/search/aggs/utils/get_format_with_aggs.test.ts rename to src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts index 3b440bc50c93b8..20d8cfc105e49d 100644 --- a/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts @@ -19,9 +19,8 @@ import { identity } from 'lodash'; -import { SerializedFieldFormat } from '../../../../../expressions/common/types'; -import { FieldFormat } from '../../../../common'; -import { IFieldFormat } from '../../../../public'; +import { SerializedFieldFormat } from 'src/plugins/expressions/common/types'; +import { FieldFormat, IFieldFormat } from '../../../../common'; import { getFormatWithAggs } from './get_format_with_aggs'; diff --git a/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts similarity index 95% rename from src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts rename to src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts index e0db249c7cf865..01369206ab3cfb 100644 --- a/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts @@ -19,9 +19,12 @@ import { i18n } from '@kbn/i18n'; -import { SerializedFieldFormat } from '../../../../../expressions/common/types'; -import { FieldFormat } from '../../../../common'; -import { FieldFormatsContentType, IFieldFormat } from '../../../../public'; +import { SerializedFieldFormat } from 'src/plugins/expressions/common/types'; +import { + FieldFormat, + FieldFormatsContentType, + IFieldFormat, +} from '../../../../common/field_formats'; import { convertDateRangeToString, DateRangeKey } from '../buckets/lib/date_range'; import { convertIPRangeToString, IpRangeKey } from '../buckets/lib/ip_range'; diff --git a/src/plugins/data/public/search/aggs/utils/get_parsed_value.ts b/src/plugins/data/common/search/aggs/utils/get_parsed_value.ts similarity index 100% rename from src/plugins/data/public/search/aggs/utils/get_parsed_value.ts rename to src/plugins/data/common/search/aggs/utils/get_parsed_value.ts diff --git a/src/plugins/data/public/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts similarity index 93% rename from src/plugins/data/public/search/aggs/utils/index.ts rename to src/plugins/data/common/search/aggs/utils/index.ts index 5a889ee9ead9d9..99ce44207d80d2 100644 --- a/src/plugins/data/public/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -18,6 +18,8 @@ */ export * from './calculate_auto_time_expression'; +export * from './date_interval_utils'; export * from './get_format_with_aggs'; +export * from './ipv4_address'; export * from './prop_filter'; export * from './to_angular_json'; diff --git a/src/plugins/data/common/search/aggs/ipv4_address.test.ts b/src/plugins/data/common/search/aggs/utils/ipv4_address.test.ts similarity index 100% rename from src/plugins/data/common/search/aggs/ipv4_address.test.ts rename to src/plugins/data/common/search/aggs/utils/ipv4_address.test.ts diff --git a/src/plugins/data/common/search/aggs/ipv4_address.ts b/src/plugins/data/common/search/aggs/utils/ipv4_address.ts similarity index 100% rename from src/plugins/data/common/search/aggs/ipv4_address.ts rename to src/plugins/data/common/search/aggs/utils/ipv4_address.ts diff --git a/src/plugins/data/public/search/aggs/utils/prop_filter.test.ts b/src/plugins/data/common/search/aggs/utils/prop_filter.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/utils/prop_filter.test.ts rename to src/plugins/data/common/search/aggs/utils/prop_filter.test.ts diff --git a/src/plugins/data/public/search/aggs/utils/prop_filter.ts b/src/plugins/data/common/search/aggs/utils/prop_filter.ts similarity index 100% rename from src/plugins/data/public/search/aggs/utils/prop_filter.ts rename to src/plugins/data/common/search/aggs/utils/prop_filter.ts diff --git a/src/plugins/data/public/search/aggs/utils/to_angular_json.ts b/src/plugins/data/common/search/aggs/utils/to_angular_json.ts similarity index 100% rename from src/plugins/data/public/search/aggs/utils/to_angular_json.ts rename to src/plugins/data/common/search/aggs/utils/to_angular_json.ts diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index f1a39a83836299..25839a805d8c58 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -18,3 +18,4 @@ */ export * from './esaggs'; +export * from './utils'; diff --git a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts b/src/plugins/data/common/search/expressions/utils/courier_inspector_stats.ts similarity index 98% rename from src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts rename to src/plugins/data/common/search/expressions/utils/courier_inspector_stats.ts index c933e8cd3e9613..d41f02d2728d5f 100644 --- a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts +++ b/src/plugins/data/common/search/expressions/utils/courier_inspector_stats.ts @@ -26,8 +26,8 @@ import { i18n } from '@kbn/i18n'; import { SearchResponse } from 'elasticsearch'; +import { ISearchSource } from 'src/plugins/data/public'; import { RequestStatistics } from 'src/plugins/inspector/common'; -import { ISearchSource } from '../../search_source'; /** @public */ export function getRequestInspectorStats(searchSource: ISearchSource) { diff --git a/src/plugins/data/common/search/expressions/utils/index.ts b/src/plugins/data/common/search/expressions/utils/index.ts new file mode 100644 index 00000000000000..75c1809770c787 --- /dev/null +++ b/src/plugins/data/common/search/expressions/utils/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './courier_inspector_stats'; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 45daf16d10c6d6..557ab64079d16f 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -17,10 +17,13 @@ * under the License. */ -import { ES_SEARCH_STRATEGY } from './es_search'; - -export { IKibanaSearchResponse, IKibanaSearchRequest } from './types'; +export * from './aggs'; +export * from './es_search'; +export * from './expressions'; +export * from './tabify'; +export * from './types'; +import { ES_SEARCH_STRATEGY } from './es_search'; export const DEFAULT_SEARCH_STRATEGY = ES_SEARCH_STRATEGY; export { diff --git a/src/plugins/data/public/search/tabify/buckets.test.ts b/src/plugins/data/common/search/tabify/buckets.test.ts similarity index 100% rename from src/plugins/data/public/search/tabify/buckets.test.ts rename to src/plugins/data/common/search/tabify/buckets.test.ts diff --git a/src/plugins/data/public/search/tabify/buckets.ts b/src/plugins/data/common/search/tabify/buckets.ts similarity index 100% rename from src/plugins/data/public/search/tabify/buckets.ts rename to src/plugins/data/common/search/tabify/buckets.ts diff --git a/src/plugins/data/public/search/tabify/get_columns.test.ts b/src/plugins/data/common/search/tabify/get_columns.test.ts similarity index 100% rename from src/plugins/data/public/search/tabify/get_columns.test.ts rename to src/plugins/data/common/search/tabify/get_columns.test.ts diff --git a/src/plugins/data/public/search/tabify/get_columns.ts b/src/plugins/data/common/search/tabify/get_columns.ts similarity index 100% rename from src/plugins/data/public/search/tabify/get_columns.ts rename to src/plugins/data/common/search/tabify/get_columns.ts diff --git a/src/plugins/data/public/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts similarity index 100% rename from src/plugins/data/public/search/tabify/index.ts rename to src/plugins/data/common/search/tabify/index.ts diff --git a/src/plugins/data/public/search/tabify/response_writer.test.ts b/src/plugins/data/common/search/tabify/response_writer.test.ts similarity index 100% rename from src/plugins/data/public/search/tabify/response_writer.test.ts rename to src/plugins/data/common/search/tabify/response_writer.test.ts diff --git a/src/plugins/data/public/search/tabify/response_writer.ts b/src/plugins/data/common/search/tabify/response_writer.ts similarity index 100% rename from src/plugins/data/public/search/tabify/response_writer.ts rename to src/plugins/data/common/search/tabify/response_writer.ts diff --git a/src/plugins/data/public/search/tabify/tabify.test.ts b/src/plugins/data/common/search/tabify/tabify.test.ts similarity index 98% rename from src/plugins/data/public/search/tabify/tabify.test.ts rename to src/plugins/data/common/search/tabify/tabify.test.ts index 0a1e99c8bb2001..6b9d520b11436a 100644 --- a/src/plugins/data/public/search/tabify/tabify.test.ts +++ b/src/plugins/data/common/search/tabify/tabify.test.ts @@ -18,7 +18,7 @@ */ import { tabifyAggResponse } from './tabify'; -import { IndexPattern } from '../../index_patterns'; +import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; import { AggConfigs, IAggConfig, IAggConfigs } from '../aggs'; import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; diff --git a/src/plugins/data/public/search/tabify/tabify.ts b/src/plugins/data/common/search/tabify/tabify.ts similarity index 100% rename from src/plugins/data/public/search/tabify/tabify.ts rename to src/plugins/data/common/search/tabify/tabify.ts diff --git a/src/plugins/data/public/search/tabify/types.ts b/src/plugins/data/common/search/tabify/types.ts similarity index 100% rename from src/plugins/data/public/search/tabify/types.ts rename to src/plugins/data/common/search/tabify/types.ts diff --git a/src/plugins/data/public/field_formats/utils/deserialize.ts b/src/plugins/data/public/field_formats/utils/deserialize.ts index 26baa5fdeb1e4b..7595a443bf8f0e 100644 --- a/src/plugins/data/public/field_formats/utils/deserialize.ts +++ b/src/plugins/data/public/field_formats/utils/deserialize.ts @@ -23,9 +23,9 @@ import { SerializedFieldFormat } from '../../../../expressions/common/types'; import { FieldFormat } from '../../../common'; import { FormatFactory } from '../../../common/field_formats/utils'; +import { getFormatWithAggs } from '../../../common/search/aggs'; import { DataPublicPluginStart, IFieldFormat } from '../../../public'; import { getUiSettings } from '../../../public/services'; -import { getFormatWithAggs } from '../../search/aggs/utils'; const getConfig = (key: string, defaultOverride?: any): any => getUiSettings().get(key, defaultOverride); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index f036d5f30a0e25..d35069207ee844 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -294,15 +294,6 @@ import { propFilter, siblingPipelineType, termsAggFilter, - // expressions utils - getRequestInspectorStats, - getResponseInspectorStats, - // tabify - tabifyAggResponse, - tabifyGetColumns, -} from './search'; - -import { dateHistogramInterval, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, @@ -312,10 +303,14 @@ import { parseEsInterval, parseInterval, toAbsoluteDates, + // expressions utils + getRequestInspectorStats, + getResponseInspectorStats, + // tabify + tabifyAggResponse, + tabifyGetColumns, } from '../common'; -export { EsaggsExpressionFunctionDefinition, ParsedInterval } from '../common'; - export { // aggs AggGroupLabels, @@ -326,6 +321,7 @@ export { AggParamType, AggConfigOptions, BUCKET_TYPES, + EsaggsExpressionFunctionDefinition, IAggConfig, IAggConfigs, IAggType, @@ -334,35 +330,39 @@ export { METRIC_TYPES, OptionedParamType, OptionedValueProp, + ParsedInterval, + // tabify + TabbedAggColumn, + TabbedAggRow, + TabbedTable, +} from '../common'; + +export { // search ES_SEARCH_STRATEGY, + EsQuerySortValue, + extractSearchSourceReferences, + FetchOptions, getEsPreference, - ISearch, - ISearchOptions, - ISearchGeneric, - IEsSearchResponse, + getSearchParamsFromRequest, IEsSearchRequest, - IKibanaSearchResponse, + IEsSearchResponse, IKibanaSearchRequest, - SearchRequest, - SearchResponse, - SearchError, + IKibanaSearchResponse, + injectSearchSourceReferences, + ISearch, + ISearchGeneric, + ISearchOptions, ISearchSource, parseSearchSourceJSON, - injectSearchSourceReferences, - getSearchParamsFromRequest, - extractSearchSourceReferences, - SearchSourceFields, - EsQuerySortValue, - SortDirection, - FetchOptions, - // tabify - TabbedAggColumn, - TabbedAggRow, - TabbedTable, + RequestTimeoutError, + SearchError, SearchInterceptor, SearchInterceptorDeps, - RequestTimeoutError, + SearchRequest, + SearchResponse, + SearchSourceFields, + SortDirection, } from './search'; // Search namespace diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 3fc1e6454829d6..78e40cfedd9060 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -79,7 +79,7 @@ const createStartContract = (): Start => { }; export { createSearchSourceMock } from './search/mocks'; -export { getCalculateAutoTimeExpression } from './search/aggs'; +export { getCalculateAutoTimeExpression } from '../common/search/aggs'; export const dataPluginMock = { createSetupContract, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index e950434b287a75..e6a48794d8b0f4 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -33,7 +33,6 @@ import { DataPublicPluginStart, DataSetupDependencies, DataStartDependencies, - InternalStartServices, DataPublicPluginEnhancements, } from './types'; import { AutocompleteService } from './autocomplete'; @@ -120,17 +119,6 @@ export class DataPublicPlugin ): DataPublicPluginSetup { const startServices = createStartServicesGetter(core.getStartServices); - const getInternalStartServices = (): InternalStartServices => { - const { core: coreStart, self } = startServices(); - return { - fieldFormats: self.fieldFormats, - notifications: coreStart.notifications, - uiSettings: coreStart.uiSettings, - searchService: self.search, - injectedMetadata: coreStart.injectedMetadata, - }; - }; - expressions.registerFunction(esaggs); expressions.registerFunction(indexPatternLoad); @@ -158,10 +146,9 @@ export class DataPublicPlugin ); const searchService = this.searchService.setup(core, { - expressions, usageCollection, - getInternalStartServices, packageInfo: this.packageInfo, + registerFunction: expressions.registerFunction, }); return { @@ -210,7 +197,7 @@ export class DataPublicPlugin }); setQueryService(query); - const search = this.searchService.start(core, { indexPatterns }); + const search = this.searchService.start(core, { fieldFormats, indexPatterns }); setSearchService(search); uiActions.addTriggerAction( @@ -247,5 +234,7 @@ export class DataPublicPlugin public stop() { this.autocomplete.clearProviders(); + this.queryService.stop(); + this.searchService.stop(); } } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index a61334905e9f58..744376403e1a19 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -26,10 +26,12 @@ import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; +import { FetchOptions as FetchOptions_2 } from 'src/plugins/data/public'; import { History } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; +import { ISearchSource as ISearchSource_2 } from 'src/plugins/data/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiSettingsClient } from 'src/core/public'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/public'; @@ -153,12 +155,12 @@ export interface ApplyGlobalFilterActionContext { timeFieldName?: string; } -// Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateNanosFormat | typeof DateFormat)[]; +export const baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat | typeof DateNanosFormat)[]; // Warning: (ae-missing-release-tag) "BUCKET_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1653,7 +1655,7 @@ export const search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("./search/aggs/buckets/bucket_agg_type").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; } | { display: string; val: string; @@ -1662,9 +1664,9 @@ export const search: { InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; Ipv4Address: typeof Ipv4Address; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; - isNumberType: (agg: import("./search").AggConfig) => boolean; - isStringType: (agg: import("./search").AggConfig) => boolean; - isType: (...types: string[]) => (agg: import("./search").AggConfig) => boolean; + isNumberType: (agg: import("../common").AggConfig) => boolean; + isStringType: (agg: import("../common").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("../common").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parentPipelineType: string; diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts new file mode 100644 index 00000000000000..db25dfb300d117 --- /dev/null +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -0,0 +1,182 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, Subscription } from 'rxjs'; + +import { coreMock } from '../../../../../core/public/mocks'; +import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks'; +import { BucketAggType, getAggTypes, MetricAggType } from '../../../common'; +import { fieldFormatsServiceMock } from '../../field_formats/mocks'; + +import { + AggsService, + AggsSetupDependencies, + AggsStartDependencies, + createGetConfig, +} from './aggs_service'; + +const { uiSettings } = coreMock.createSetup(); + +describe('AggsService - public', () => { + let service: AggsService; + let setupDeps: AggsSetupDependencies; + let startDeps: AggsStartDependencies; + + beforeEach(() => { + service = new AggsService(); + setupDeps = { + registerFunction: expressionsPluginMock.createSetupContract().registerFunction, + uiSettings, + }; + startDeps = { + fieldFormats: fieldFormatsServiceMock.createStartContract(), + uiSettings, + }; + }); + + describe('setup()', () => { + test('exposes proper contract', () => { + const setup = service.setup(setupDeps); + expect(Object.keys(setup).length).toBe(1); + expect(setup).toHaveProperty('types'); + }); + + test('registers default agg types', () => { + service.setup(setupDeps); + const start = service.start(startDeps); + expect(start.types.getAll().buckets.length).toBe(11); + expect(start.types.getAll().metrics.length).toBe(21); + }); + + test('registers custom agg types', () => { + const setup = service.setup(setupDeps); + setup.types.registerBucket( + 'foo', + () => ({ name: 'foo', type: 'buckets' } as BucketAggType) + ); + setup.types.registerMetric( + 'bar', + () => ({ name: 'bar', type: 'metrics' } as MetricAggType) + ); + + const start = service.start(startDeps); + expect(start.types.getAll().buckets.length).toBe(12); + expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); + expect(start.types.getAll().metrics.length).toBe(22); + expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); + }); + }); + + describe('start()', () => { + test('exposes proper contract', () => { + const start = service.start(startDeps); + expect(Object.keys(start).length).toBe(3); + expect(start).toHaveProperty('calculateAutoTimeExpression'); + expect(start).toHaveProperty('createAggConfigs'); + expect(start).toHaveProperty('types'); + }); + + test('types registry returns initialized agg types', () => { + service.setup(setupDeps); + const start = service.start(startDeps); + + expect(start.types.get('terms').name).toBe('terms'); + }); + + test('registers default agg types', () => { + service.setup(setupDeps); + const start = service.start(startDeps); + + const aggTypes = getAggTypes(); + expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length); + expect(start.types.getAll().metrics.length).toBe(aggTypes.metrics.length); + }); + + test('merges default agg types with types registered during setup', () => { + const setup = service.setup(setupDeps); + setup.types.registerBucket( + 'foo', + () => ({ name: 'foo', type: 'buckets' } as BucketAggType) + ); + setup.types.registerMetric( + 'bar', + () => ({ name: 'bar', type: 'metrics' } as MetricAggType) + ); + + const start = service.start(startDeps); + + const aggTypes = getAggTypes(); + expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length + 1); + expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); + expect(start.types.getAll().metrics.length).toBe(aggTypes.metrics.length + 1); + expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); + }); + }); + + describe('createGetConfig()', () => { + let fooSubject$: BehaviorSubject; + let barSubject$: BehaviorSubject; + + beforeEach(() => { + jest.clearAllMocks(); + + fooSubject$ = new BehaviorSubject('fooVal'); + barSubject$ = new BehaviorSubject('barVal'); + + uiSettings.get$.mockImplementation((key: string) => { + const mockSettings: Record = { + foo: () => fooSubject$, + bar: () => barSubject$, + }; + return mockSettings[key] ? mockSettings[key]() : undefined; + }); + }); + + test('returns a function to get the value of the provided setting', () => { + const requiredSettings = ['foo', 'bar']; + const subscriptions: Subscription[] = []; + const getConfig = createGetConfig(uiSettings, requiredSettings, subscriptions); + + expect(getConfig('foo')).toBe('fooVal'); + expect(getConfig('bar')).toBe('barVal'); + }); + + test('does not return values for settings that are not explicitly declared', () => { + const requiredSettings = ['foo', 'bar']; + const subscriptions: Subscription[] = []; + const getConfig = createGetConfig(uiSettings, requiredSettings, subscriptions); + + expect(subscriptions.length).toBe(2); + expect(getConfig('baz')).toBe(undefined); + }); + + test('provides latest value for each setting', () => { + const requiredSettings = ['foo', 'bar']; + const subscriptions: Subscription[] = []; + const getConfig = createGetConfig(uiSettings, requiredSettings, subscriptions); + + expect(getConfig('foo')).toBe('fooVal'); + fooSubject$.next('fooVal2'); + expect(getConfig('foo')).toBe('fooVal2'); + expect(getConfig('foo')).toBe('fooVal2'); + fooSubject$.next('fooVal3'); + expect(getConfig('foo')).toBe('fooVal3'); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts new file mode 100644 index 00000000000000..d535f97fefdf85 --- /dev/null +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -0,0 +1,152 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Subscription } from 'rxjs'; + +import { IUiSettingsClient } from 'src/core/public'; +import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; +import { + aggsRequiredUiSettings, + AggsCommonStartDependencies, + AggsCommonService, + AggConfigs, + AggTypesDependencies, + calculateBounds, + TimeRange, +} from '../../../common'; +import { FieldFormatsStart } from '../../field_formats'; +import { getForceNow } from '../../query/timefilter/lib/get_force_now'; +import { AggsSetup, AggsStart } from './types'; + +/** + * Aggs needs synchronous access to specific uiSettings. Since settings can change + * without a page refresh, we create a cache that subscribes to changes from + * uiSettings.get$ and keeps everything up-to-date. + * + * @internal + */ +export function createGetConfig( + uiSettings: IUiSettingsClient, + requiredSettings: string[], + subscriptions: Subscription[] +): AggsCommonStartDependencies['getConfig'] { + const settingsCache: Record = {}; + + requiredSettings.forEach((setting) => { + subscriptions.push( + uiSettings.get$(setting).subscribe((value) => { + settingsCache[setting] = value; + }) + ); + }); + + return (key) => settingsCache[key]; +} + +/** @internal */ +export interface AggsSetupDependencies { + registerFunction: ExpressionsServiceSetup['registerFunction']; + uiSettings: IUiSettingsClient; +} + +/** @internal */ +export interface AggsStartDependencies { + fieldFormats: FieldFormatsStart; + uiSettings: IUiSettingsClient; +} + +/** + * The aggs service provides a means of modeling and manipulating the various + * Elasticsearch aggregations supported by Kibana, providing the ability to + * output the correct DSL when you are ready to send your request to ES. + */ +export class AggsService { + private readonly aggsCommonService = new AggsCommonService(); + private readonly initializedAggTypes = new Map(); + private getConfig?: AggsCommonStartDependencies['getConfig']; + private subscriptions: Subscription[] = []; + + /** + * getForceNow uses window.location, so we must have a separate implementation + * of calculateBounds on the client and the server. + */ + private calculateBounds = (timeRange: TimeRange) => + calculateBounds(timeRange, { forceNow: getForceNow() }); + + public setup({ registerFunction, uiSettings }: AggsSetupDependencies): AggsSetup { + this.getConfig = createGetConfig(uiSettings, aggsRequiredUiSettings, this.subscriptions); + + return this.aggsCommonService.setup({ registerFunction }); + } + + public start({ fieldFormats, uiSettings }: AggsStartDependencies): AggsStart { + const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ + getConfig: this.getConfig!, + }); + + const aggTypesDependencies: AggTypesDependencies = { + calculateBounds: this.calculateBounds, + getConfig: this.getConfig!, + getFieldFormatsStart: () => ({ + deserialize: fieldFormats.deserialize, + getDefaultInstance: fieldFormats.getDefaultInstance, + }), + isDefaultTimezone: () => uiSettings.isDefault('dateFormat:tz'), + }; + + // initialize each agg type and store in memory + types.getAll().buckets.forEach((type) => { + const agg = type(aggTypesDependencies); + this.initializedAggTypes.set(agg.name, agg); + }); + types.getAll().metrics.forEach((type) => { + const agg = type(aggTypesDependencies); + this.initializedAggTypes.set(agg.name, agg); + }); + + const typesRegistry = { + get: (name: string) => { + return this.initializedAggTypes.get(name); + }, + getAll: () => { + return { + buckets: Array.from(this.initializedAggTypes.values()).filter( + (agg) => agg.type === 'buckets' + ), + metrics: Array.from(this.initializedAggTypes.values()).filter( + (agg) => agg.type === 'metrics' + ), + }; + }, + }; + + return { + calculateAutoTimeExpression, + createAggConfigs: (indexPattern, configStates = [], schemas) => { + return new AggConfigs(indexPattern, configStates, { typesRegistry }); + }, + types: typesRegistry, + }; + } + + public stop() { + this.subscriptions.forEach((s) => s.unsubscribe()); + this.subscriptions = []; + } +} diff --git a/src/plugins/data/public/search/aggs/index.ts b/src/plugins/data/public/search/aggs/index.ts index 1139d9c7ff7228..77d97c426260c9 100644 --- a/src/plugins/data/public/search/aggs/index.ts +++ b/src/plugins/data/public/search/aggs/index.ts @@ -17,14 +17,5 @@ * under the License. */ -export * from './agg_config'; -export * from './agg_configs'; -export * from './agg_groups'; -export * from './agg_type'; -export * from './agg_types'; -export * from './agg_types_registry'; -export * from './buckets'; -export * from './metrics'; -export * from './param_types'; +export * from './aggs_service'; export * from './types'; -export * from './utils'; diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index 2cad6460672927..ca13343777e63f 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -17,15 +17,17 @@ * under the License. */ -import { coreMock } from '../../../../../../src/core/public/mocks'; import { AggConfigs, AggTypesRegistrySetup, AggTypesRegistryStart, getCalculateAutoTimeExpression, -} from './'; -import { SearchAggsSetup, SearchAggsStart } from './types'; -import { mockAggTypesRegistry } from './test_helpers'; +} from '../../../common'; +import { AggsSetup, AggsStart } from './types'; + +import { mockAggTypesRegistry } from '../../../common/search/aggs/test_helpers'; + +const getConfig = jest.fn(); const aggTypeBaseParamMock = () => ({ name: 'some_param', @@ -53,21 +55,18 @@ export const aggTypesRegistrySetupMock = (): AggTypesRegistrySetup => ({ export const aggTypesRegistryStartMock = (): AggTypesRegistryStart => ({ get: jest.fn().mockImplementation(aggTypeConfigMock), - getBuckets: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), - getMetrics: jest.fn().mockImplementation(() => [aggTypeConfigMock()]), getAll: jest.fn().mockImplementation(() => ({ buckets: [aggTypeConfigMock()], metrics: [aggTypeConfigMock()], })), }); -export const searchAggsSetupMock = (): SearchAggsSetup => ({ - calculateAutoTimeExpression: getCalculateAutoTimeExpression(coreMock.createSetup().uiSettings), +export const searchAggsSetupMock = (): AggsSetup => ({ types: aggTypesRegistrySetupMock(), }); -export const searchAggsStartMock = (): SearchAggsStart => ({ - calculateAutoTimeExpression: getCalculateAutoTimeExpression(coreMock.createStart().uiSettings), +export const searchAggsStartMock = (): AggsStart => ({ + calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index a784bfaada4c75..38be541973ce93 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -17,131 +17,7 @@ * under the License. */ -import { IndexPattern } from '../../index_patterns'; -import { - AggConfigSerialized, - AggConfigs, - AggParamsRange, - AggParamsIpRange, - AggParamsDateRange, - AggParamsFilter, - AggParamsFilters, - AggParamsSignificantTerms, - AggParamsGeoTile, - AggParamsGeoHash, - AggParamsTerms, - AggParamsAvg, - AggParamsCardinality, - AggParamsGeoBounds, - AggParamsGeoCentroid, - AggParamsMax, - AggParamsMedian, - AggParamsMin, - AggParamsStdDeviation, - AggParamsSum, - AggParamsBucketAvg, - AggParamsBucketMax, - AggParamsBucketMin, - AggParamsBucketSum, - AggParamsCumulativeSum, - AggParamsDerivative, - AggParamsMovingAvg, - AggParamsPercentileRanks, - AggParamsPercentiles, - AggParamsSerialDiff, - AggParamsTopHit, - AggParamsHistogram, - AggParamsDateHistogram, - AggTypesRegistrySetup, - AggTypesRegistryStart, - CreateAggConfigParams, - getCalculateAutoTimeExpression, - METRIC_TYPES, - BUCKET_TYPES, -} from './'; +import { AggsCommonSetup } from '../../../common'; -export { IAggConfig, AggConfigSerialized } from './agg_config'; -export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; -export { IAggType } from './agg_type'; -export { AggParam, AggParamOption } from './agg_params'; -export { IFieldParamType } from './param_types'; -export { IMetricAggType } from './metrics/metric_agg_type'; -export { DateRangeKey } from './buckets/lib/date_range'; -export { IpRangeKey } from './buckets/lib/ip_range'; -export { OptionedValueProp } from './param_types/optioned'; - -/** @internal */ -export interface SearchAggsSetup { - calculateAutoTimeExpression: ReturnType; - types: AggTypesRegistrySetup; -} - -/** @internal */ -export interface SearchAggsStart { - calculateAutoTimeExpression: ReturnType; - createAggConfigs: ( - indexPattern: IndexPattern, - configStates?: CreateAggConfigParams[], - schemas?: Record - ) => InstanceType; - types: AggTypesRegistryStart; -} - -/** @internal */ -export interface BaseAggParams { - json?: string; - customLabel?: string; -} - -/** @internal */ -export interface AggExpressionType { - type: 'agg_type'; - value: AggConfigSerialized; -} - -/** @internal */ -export type AggExpressionFunctionArgs< - Name extends keyof AggParamsMapping -> = AggParamsMapping[Name] & Pick; - -/** - * A global list of the param interfaces for each agg type. - * For now this is internal, but eventually we will probably - * want to make it public. - * - * @internal - */ -export interface AggParamsMapping { - [BUCKET_TYPES.RANGE]: AggParamsRange; - [BUCKET_TYPES.IP_RANGE]: AggParamsIpRange; - [BUCKET_TYPES.DATE_RANGE]: AggParamsDateRange; - [BUCKET_TYPES.FILTER]: AggParamsFilter; - [BUCKET_TYPES.FILTERS]: AggParamsFilters; - [BUCKET_TYPES.SIGNIFICANT_TERMS]: AggParamsSignificantTerms; - [BUCKET_TYPES.GEOTILE_GRID]: AggParamsGeoTile; - [BUCKET_TYPES.GEOHASH_GRID]: AggParamsGeoHash; - [BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram; - [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; - [BUCKET_TYPES.TERMS]: AggParamsTerms; - [METRIC_TYPES.AVG]: AggParamsAvg; - [METRIC_TYPES.CARDINALITY]: AggParamsCardinality; - [METRIC_TYPES.COUNT]: BaseAggParams; - [METRIC_TYPES.GEO_BOUNDS]: AggParamsGeoBounds; - [METRIC_TYPES.GEO_CENTROID]: AggParamsGeoCentroid; - [METRIC_TYPES.MAX]: AggParamsMax; - [METRIC_TYPES.MEDIAN]: AggParamsMedian; - [METRIC_TYPES.MIN]: AggParamsMin; - [METRIC_TYPES.STD_DEV]: AggParamsStdDeviation; - [METRIC_TYPES.SUM]: AggParamsSum; - [METRIC_TYPES.AVG_BUCKET]: AggParamsBucketAvg; - [METRIC_TYPES.MAX_BUCKET]: AggParamsBucketMax; - [METRIC_TYPES.MIN_BUCKET]: AggParamsBucketMin; - [METRIC_TYPES.SUM_BUCKET]: AggParamsBucketSum; - [METRIC_TYPES.CUMULATIVE_SUM]: AggParamsCumulativeSum; - [METRIC_TYPES.DERIVATIVE]: AggParamsDerivative; - [METRIC_TYPES.MOVING_FN]: AggParamsMovingAvg; - [METRIC_TYPES.PERCENTILE_RANKS]: AggParamsPercentileRanks; - [METRIC_TYPES.PERCENTILES]: AggParamsPercentiles; - [METRIC_TYPES.SERIAL_DIFF]: AggParamsSerialDiff; - [METRIC_TYPES.TOP_HITS]: AggParamsTopHit; -} +export type AggsSetup = AggsCommonSetup; +export { AggsStart } from '../../../common'; diff --git a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 75a4464a8e61ee..7eff6f25fd8287 100644 --- a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,8 +19,8 @@ import { set } from '@elastic/safer-lodash-set'; import { FormattedData } from '../../../../../plugins/inspector/public'; +import { TabbedTable } from '../../../common'; import { FormatFactory } from '../../../common/field_formats/utils'; -import { TabbedTable } from '../tabify'; import { createFilter } from './create_filter'; /** diff --git a/src/plugins/data/public/search/expressions/create_filter.test.ts b/src/plugins/data/public/search/expressions/create_filter.test.ts index a7fd67983cb926..7968c806285318 100644 --- a/src/plugins/data/public/search/expressions/create_filter.test.ts +++ b/src/plugins/data/public/search/expressions/create_filter.test.ts @@ -17,11 +17,17 @@ * under the License. */ +import { + AggConfigs, + IAggConfig, + TabbedTable, + isRangeFilter, + BytesFormat, + FieldFormatsGetConfigFn, +} from '../../../common'; +import { mockAggTypesRegistry } from '../../../common/search/aggs/test_helpers'; + import { createFilter } from './create_filter'; -import { AggConfigs, IAggConfig } from '../aggs'; -import { TabbedTable } from '../tabify'; -import { isRangeFilter, BytesFormat, FieldFormatsGetConfigFn } from '../../../common'; -import { mockAggTypesRegistry } from '../aggs/test_helpers'; describe('createFilter', () => { let table: TabbedTable; diff --git a/src/plugins/data/public/search/expressions/create_filter.ts b/src/plugins/data/public/search/expressions/create_filter.ts index 94d84380e03df9..09200c2e17b319 100644 --- a/src/plugins/data/public/search/expressions/create_filter.ts +++ b/src/plugins/data/public/search/expressions/create_filter.ts @@ -17,9 +17,7 @@ * under the License. */ -import { IAggConfig } from '../aggs'; -import { TabbedTable } from '../tabify'; -import { Filter } from '../../../common'; +import { Filter, IAggConfig, TabbedTable } from '../../../common'; const getOtherBucketFilterTerms = (table: TabbedTable, columnIndex: number, rowIndex: number) => { if (rowIndex === -1) { diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 690f6b1df11c36..50fbb114b39fd8 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -19,15 +19,11 @@ import { get, hasIn } from 'lodash'; import { i18n } from '@kbn/i18n'; - import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; import { calculateObjectHash } from '../../../../../plugins/kibana_utils/public'; import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; -import { IAggConfigs } from '../aggs'; -import { ISearchSource } from '../search_source'; -import { tabifyAggResponse } from '../tabify'; import { calculateBounds, EsaggsExpressionFunctionDefinition, @@ -38,6 +34,13 @@ import { Query, TimeRange, } from '../../../common'; +import { + getRequestInspectorStats, + getResponseInspectorStats, + IAggConfigs, + tabifyAggResponse, +} from '../../../common/search'; + import { FilterManager } from '../../query'; import { getFieldFormats, @@ -45,8 +48,9 @@ import { getQueryService, getSearchService, } from '../../services'; +import { ISearchSource } from '../search_source'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; -import { getRequestInspectorStats, getResponseInspectorStats, serializeAggConfig } from './utils'; +import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; diff --git a/src/plugins/data/public/search/expressions/utils/index.ts b/src/plugins/data/public/search/expressions/utils/index.ts index 0fd51f3e158a62..094536fc18437b 100644 --- a/src/plugins/data/public/search/expressions/utils/index.ts +++ b/src/plugins/data/public/search/expressions/utils/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './courier_inspector_stats'; export * from './serialize_agg_config'; diff --git a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts index 78b4935077d103..6ba323b65783fa 100644 --- a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts +++ b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts @@ -18,7 +18,7 @@ */ import { KibanaDatatableColumnMeta } from '../../../../../../plugins/expressions/public'; -import { IAggConfig } from '../../aggs'; +import { IAggConfig } from '../../../../common'; import { IndexPattern } from '../../../index_patterns'; import { getSearchService } from '../../../../public/services'; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index ae028df31e401e..32bcd8a279036b 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -17,9 +17,7 @@ * under the License. */ -export * from './aggs'; export * from './expressions'; -export * from './tabify'; export { ISearch, diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index f0a017847e06aa..e6897a16a353a4 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -19,9 +19,8 @@ import { coreMock } from '../../../../core/public/mocks'; import { CoreSetup, CoreStart } from '../../../../core/public'; -import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks'; -import { SearchService } from './search_service'; +import { SearchService, SearchServiceSetupDependencies } from './search_service'; describe('Search service', () => { let searchService: SearchService; @@ -36,11 +35,12 @@ describe('Search service', () => { describe('setup()', () => { it('exposes proper contract', async () => { - const setup = searchService.setup(mockCoreSetup, { + const setup = searchService.setup(mockCoreSetup, ({ packageInfo: { version: '8' }, - expressions: expressionsPluginMock.createSetupContract(), - } as any); + registerFunction: jest.fn(), + } as unknown) as SearchServiceSetupDependencies); expect(setup).toHaveProperty('aggs'); + expect(setup).toHaveProperty('usageCollector'); expect(setup).toHaveProperty('__enhance'); }); }); @@ -50,6 +50,7 @@ describe('Search service', () => { const start = searchService.start(mockCoreStart, { indexPatterns: {}, } as any); + expect(start).toHaveProperty('aggs'); expect(start).toHaveProperty('search'); }); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 4c94925b66d6e2..bd9c1b1253fe2d 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -17,80 +17,43 @@ * under the License. */ -import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public'; +import { Plugin, CoreSetup, CoreStart, PackageInfo } from 'src/core/public'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; -import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source'; import { getEsClient, LegacyApiCaller } from './legacy'; -import { getForceNow } from '../query/timefilter/lib/get_force_now'; -import { calculateBounds, TimeRange } from '../../common/query'; - +import { AggsService, AggsSetupDependencies, AggsStartDependencies } from './aggs'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; -import { GetInternalStartServicesFn } from '../types'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; -import { - getAggTypes, - getAggTypesFunctions, - AggTypesRegistry, - AggConfigs, - getCalculateAutoTimeExpression, -} from './aggs'; import { ISearchGeneric } from './types'; import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -interface SearchServiceSetupDependencies { - expressions: ExpressionsSetup; - usageCollection?: UsageCollectionSetup; - getInternalStartServices: GetInternalStartServicesFn; +/** @internal */ +export interface SearchServiceSetupDependencies { packageInfo: PackageInfo; + registerFunction: AggsSetupDependencies['registerFunction']; + usageCollection?: UsageCollectionSetup; } -interface SearchServiceStartDependencies { +/** @internal */ +export interface SearchServiceStartDependencies { + fieldFormats: AggsStartDependencies['fieldFormats']; indexPatterns: IndexPatternsContract; } export class SearchService implements Plugin { private esClient?: LegacyApiCaller; - private readonly aggTypesRegistry = new AggTypesRegistry(); + private readonly aggsService = new AggsService(); private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; - /** - * getForceNow uses window.location, so we must have a separate implementation - * of calculateBounds on the client and the server. - */ - private calculateBounds = (timeRange: TimeRange) => - calculateBounds(timeRange, { forceNow: getForceNow() }); - public setup( core: CoreSetup, - { - expressions, - usageCollection, - packageInfo, - getInternalStartServices, - }: SearchServiceSetupDependencies + { packageInfo, registerFunction, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { this.usageCollector = createUsageCollector(core, usageCollection); this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo); - - const aggTypesSetup = this.aggTypesRegistry.setup(); - - // register each agg type - const aggTypes = getAggTypes({ - calculateBounds: this.calculateBounds, - getInternalStartServices, - uiSettings: core.uiSettings, - }); - aggTypes.buckets.forEach((b) => aggTypesSetup.registerBucket(b)); - aggTypes.metrics.forEach((m) => aggTypesSetup.registerMetric(m)); - - // register expression functions for each agg type - const aggFunctions = getAggTypesFunctions(); - aggFunctions.forEach((fn) => expressions.registerFunction(fn)); - /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -109,20 +72,21 @@ export class SearchService implements Plugin { ); return { + aggs: this.aggsService.setup({ + registerFunction, + uiSettings: core.uiSettings, + }), usageCollector: this.usageCollector!, __enhance: (enhancements: SearchEnhancements) => { this.searchInterceptor = enhancements.searchInterceptor; }, - aggs: { - calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), - types: aggTypesSetup, - }, }; } - public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart { - const aggTypesStart = this.aggTypesRegistry.start(); - + public start( + { application, http, injectedMetadata, notifications, uiSettings }: CoreStart, + { fieldFormats, indexPatterns }: SearchServiceStartDependencies + ): ISearchStart { const search: ISearchGeneric = (request, options) => { return this.searchInterceptor.search(request, options); }; @@ -132,25 +96,17 @@ export class SearchService implements Plugin { }; const searchSourceDependencies: SearchSourceDependencies = { - uiSettings: core.uiSettings, - injectedMetadata: core.injectedMetadata, + uiSettings, + injectedMetadata, search, legacySearch, }; return { - aggs: { - calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), - createAggConfigs: (indexPattern, configStates = [], schemas) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: aggTypesStart, - }); - }, - types: aggTypesStart, - }, + aggs: this.aggsService.start({ fieldFormats, uiSettings }), search, searchSource: { - create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies), + create: createSearchSource(indexPatterns, searchSourceDependencies), createEmpty: () => { return new SearchSource({}, searchSourceDependencies); }, @@ -159,5 +115,7 @@ export class SearchService implements Plugin { }; } - public stop() {} + public stop() { + this.aggsService.stop(); + } } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index d85d4c4e5c9354..d1a44379434023 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -19,11 +19,11 @@ import { Observable } from 'rxjs'; import { PackageInfo } from 'kibana/server'; -import { SearchAggsSetup, SearchAggsStart } from './aggs'; import { LegacyApiCaller } from './legacy/es_client'; import { ISearchInterceptor } from './search_interceptor'; import { ISearchSource, SearchSourceFields } from './search_source'; import { SearchUsageCollector } from './collectors'; +import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; import { IKibanaSearchRequest, IKibanaSearchResponse, @@ -31,9 +31,7 @@ import { IEsSearchResponse, } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; -import { ExpressionsSetup } from '../../../expressions/public'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -import { GetInternalStartServicesFn } from '../types'; export interface ISearchOptions { signal?: AbortSignal; @@ -62,7 +60,7 @@ export interface SearchEnhancements { * point. */ export interface ISearchSetup { - aggs: SearchAggsSetup; + aggs: AggsSetup; usageCollector?: SearchUsageCollector; /** * @internal @@ -71,7 +69,7 @@ export interface ISearchSetup { } export interface ISearchStart { - aggs: SearchAggsStart; + aggs: AggsStart; search: ISearchGeneric; searchSource: { create: (fields?: SearchSourceFields) => Promise; @@ -86,13 +84,15 @@ export interface ISearchStart { export { SEARCH_EVENT_TYPE } from './collectors'; +/** @internal */ export interface SearchServiceSetupDependencies { - expressions: ExpressionsSetup; - usageCollection?: UsageCollectionSetup; - getInternalStartServices: GetInternalStartServicesFn; packageInfo: PackageInfo; + registerFunction: AggsSetupDependencies['registerFunction']; + usageCollection?: UsageCollectionSetup; } +/** @internal */ export interface SearchServiceStartDependencies { + fieldFormats: AggsStartDependencies['fieldFormats']; indexPatterns: IndexPatternsContract; } diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index c39b7d355d4954..bffc10642eb471 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -82,15 +82,3 @@ export interface IDataPluginServices extends Partial { storage: IStorageWrapper; data: DataPublicPluginStart; } - -/** @internal **/ -export interface InternalStartServices { - readonly fieldFormats: FieldFormatsStart; - readonly notifications: CoreStart['notifications']; - readonly uiSettings: CoreStart['uiSettings']; - readonly searchService: DataPublicPluginStart['search']; - readonly injectedMetadata: CoreStart['injectedMetadata']; -} - -/** @internal **/ -export type GetInternalStartServicesFn = () => InternalStartServices; diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 73ed88850d7874..c3b06992dba0e2 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -150,6 +150,16 @@ export { */ import { + // aggs + CidrMask, + intervalOptions, + isNumberType, + isStringType, + isType, + parentPipelineType, + propFilter, + siblingPipelineType, + termsAggFilter, dateHistogramInterval, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, @@ -159,13 +169,41 @@ import { parseEsInterval, parseInterval, toAbsoluteDates, + // expressions utils + getRequestInspectorStats, + getResponseInspectorStats, + // tabify + tabifyAggResponse, + tabifyGetColumns, } from '../common'; export { + // aggs + AggGroupLabels, + AggGroupName, + AggGroupNames, + AggParam, + AggParamOption, + AggParamType, + AggConfigOptions, + BUCKET_TYPES, EsaggsExpressionFunctionDefinition, + IAggConfig, + IAggConfigs, + IAggType, + IFieldParamType, + IMetricAggType, + METRIC_TYPES, + OptionedParamType, + OptionedValueProp, ParsedInterval, + // search IEsSearchRequest, IEsSearchResponse, + // tabify + TabbedAggColumn, + TabbedAggRow, + TabbedTable, } from '../common'; export { @@ -182,16 +220,29 @@ export { // Search namespace export const search = { aggs: { + CidrMask, dateHistogramInterval, + intervalOptions, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, Ipv4Address, + isNumberType, + isStringType, + isType, isValidEsInterval, isValidInterval, + parentPipelineType, parseEsInterval, parseInterval, + propFilter, + siblingPipelineType, + termsAggFilter, toAbsoluteDates, }, + getRequestInspectorStats, + getResponseInspectorStats, + tabifyAggResponse, + tabifyGetColumns, }; /** diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 61d8e566d2d2b8..5163bfcb17d405 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -17,13 +17,8 @@ * under the License. */ -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - Logger, -} from '../../../core/server'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'src/core/server'; +import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ConfigSchema } from '../config'; import { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns'; import { ISearchSetup, ISearchStart } from './search'; @@ -48,10 +43,21 @@ export interface DataPluginStart { } export interface DataPluginSetupDependencies { + expressions: ExpressionsServerSetup; usageCollection?: UsageCollectionSetup; } -export class DataServerPlugin implements Plugin { +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataPluginStartDependencies {} + +export class DataServerPlugin + implements + Plugin< + DataPluginSetup, + DataPluginStart, + DataPluginSetupDependencies, + DataPluginStartDependencies + > { private readonly searchService: SearchService; private readonly scriptsService: ScriptsService; private readonly kqlTelemetryService: KqlTelemetryService; @@ -70,8 +76,8 @@ export class DataServerPlugin implements Plugin, - { usageCollection }: DataPluginSetupDependencies + core: CoreSetup, + { expressions, usageCollection }: DataPluginSetupDependencies ) { this.indexPatterns.setup(core); this.scriptsService.setup(core); @@ -82,7 +88,10 @@ export class DataServerPlugin implements Plugin { + let service: AggsService; + let setupDeps: AggsSetupDependencies; + let startDeps: AggsStartDependencies; + + beforeEach(() => { + service = new AggsService(); + setupDeps = { + registerFunction: expressionsPluginMock.createSetupContract().registerFunction, + }; + startDeps = { + fieldFormats: createFieldFormatsStartMock(), + uiSettings, + }; + }); + + describe('setup()', () => { + test('exposes proper contract', () => { + const setup = service.setup(setupDeps); + expect(Object.keys(setup).length).toBe(1); + expect(setup).toHaveProperty('types'); + }); + }); + + describe('start()', () => { + test('exposes proper contract', async () => { + service.setup(setupDeps); + const start = service.start(startDeps); + + expect(Object.keys(start).length).toBe(1); + expect(start).toHaveProperty('asScopedToClient'); + + const contract = await start.asScopedToClient( + savedObjects.getScopedClient({} as KibanaRequest) + ); + expect(contract).toHaveProperty('calculateAutoTimeExpression'); + expect(contract).toHaveProperty('createAggConfigs'); + expect(contract).toHaveProperty('types'); + }); + + test('types registry returns initialized agg types', async () => { + service.setup(setupDeps); + const start = await service + .start(startDeps) + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); + + expect(start.types.get('terms').name).toBe('terms'); + }); + + test('registers default agg types', async () => { + service.setup(setupDeps); + const start = await service + .start(startDeps) + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); + + const aggTypes = getAggTypes(); + expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length); + expect(start.types.getAll().metrics.length).toBe(aggTypes.metrics.length); + }); + + test('merges default agg types with types registered during setup', async () => { + const setup = service.setup(setupDeps); + setup.types.registerBucket( + 'foo', + () => ({ name: 'foo', type: 'buckets' } as BucketAggType) + ); + setup.types.registerMetric( + 'bar', + () => ({ name: 'bar', type: 'metrics' } as MetricAggType) + ); + + const start = await service + .start(startDeps) + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); + + const aggTypes = getAggTypes(); + expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length + 1); + expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); + expect(start.types.getAll().metrics.length).toBe(aggTypes.metrics.length + 1); + expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); + }); + }); +}); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts new file mode 100644 index 00000000000000..3e5cd8adb44a66 --- /dev/null +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { pick } from 'lodash'; + +import { UiSettingsServiceStart, SavedObjectsClientContract } from 'src/core/server'; +import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; +import { + AggsCommonService, + AggConfigs, + AggTypesDependencies, + aggsRequiredUiSettings, + calculateBounds, + TimeRange, +} from '../../../common'; +import { FieldFormatsStart } from '../../field_formats'; +import { AggsSetup, AggsStart } from './types'; + +/** @internal */ +export interface AggsSetupDependencies { + registerFunction: ExpressionsServiceSetup['registerFunction']; +} + +/** @internal */ +export interface AggsStartDependencies { + fieldFormats: FieldFormatsStart; + uiSettings: UiSettingsServiceStart; +} + +/** + * The aggs service provides a means of modeling and manipulating the various + * Elasticsearch aggregations supported by Kibana, providing the ability to + * output the correct DSL when you are ready to send your request to ES. + */ +export class AggsService { + private readonly aggsCommonService = new AggsCommonService(); + + /** + * getForceNow uses window.location on the client, so we must have a + * separate implementation of calculateBounds on the server. + */ + private calculateBounds = (timeRange: TimeRange) => calculateBounds(timeRange, {}); + + public setup({ registerFunction }: AggsSetupDependencies): AggsSetup { + return this.aggsCommonService.setup({ registerFunction }); + } + + public start({ fieldFormats, uiSettings }: AggsStartDependencies): AggsStart { + return { + asScopedToClient: async (savedObjectsClient: SavedObjectsClientContract) => { + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); + + // cache ui settings, only including items which are explicitly needed by aggs + const uiSettingsCache = pick(await uiSettingsClient.getAll(), aggsRequiredUiSettings); + const getConfig = (key: string): T => { + return uiSettingsCache[key]; + }; + + const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ getConfig }); + + const aggTypesDependencies: AggTypesDependencies = { + calculateBounds: this.calculateBounds, + getConfig, + getFieldFormatsStart: () => ({ + deserialize: formats.deserialize, + getDefaultInstance: formats.getDefaultInstance, + }), + /** + * Date histogram and date range need to know whether we are using the + * default timezone, but `isDefault` is not currently offered on the + * server, so we need to manually check for the default value. + */ + isDefaultTimezone: () => getConfig('dateFormat:tz') === 'Browser', + }; + + const typesRegistry = { + get: (name: string) => { + const type = types.get(name); + if (!type) { + return; + } + return type(aggTypesDependencies); + }, + getAll: () => { + return { + // initialize each agg type on the fly + buckets: types.getAll().buckets.map((type) => type(aggTypesDependencies)), + metrics: types.getAll().metrics.map((type) => type(aggTypesDependencies)), + }; + }, + }; + + return { + calculateAutoTimeExpression, + createAggConfigs: (indexPattern, configStates = [], schemas) => { + return new AggConfigs(indexPattern, configStates, { typesRegistry }); + }, + types: typesRegistry, + }; + }, + }; + } + + public stop() {} +} diff --git a/src/plugins/data/server/search/aggs/index.ts b/src/plugins/data/server/search/aggs/index.ts new file mode 100644 index 00000000000000..77d97c426260c9 --- /dev/null +++ b/src/plugins/data/server/search/aggs/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './aggs_service'; +export * from './types'; diff --git a/src/plugins/data/server/search/aggs/mocks.ts b/src/plugins/data/server/search/aggs/mocks.ts new file mode 100644 index 00000000000000..b50e22fe87b7ce --- /dev/null +++ b/src/plugins/data/server/search/aggs/mocks.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + AggConfigs, + AggTypesRegistrySetup, + AggTypesRegistryStart, + AggsCommonStart, + getCalculateAutoTimeExpression, +} from '../../../common'; +import { AggsSetup, AggsStart } from './types'; + +import { mockAggTypesRegistry } from '../../../common/search/aggs/test_helpers'; + +const getConfig = jest.fn(); + +const aggTypeBaseParamMock = () => ({ + name: 'some_param', + type: 'some_param_type', + displayName: 'some_agg_type_param', + required: false, + advanced: false, + default: {}, + write: jest.fn(), + serialize: jest.fn().mockImplementation(() => {}), + deserialize: jest.fn().mockImplementation(() => {}), + options: [], +}); + +const aggTypeConfigMock = () => ({ + name: 'some_name', + title: 'some_title', + params: [aggTypeBaseParamMock()], +}); + +export const aggTypesRegistrySetupMock = (): AggTypesRegistrySetup => ({ + registerBucket: jest.fn(), + registerMetric: jest.fn(), +}); + +export const aggTypesRegistryStartMock = (): AggTypesRegistryStart => ({ + get: jest.fn().mockImplementation(aggTypeConfigMock), + getAll: jest.fn().mockImplementation(() => ({ + buckets: [aggTypeConfigMock()], + metrics: [aggTypeConfigMock()], + })), +}); + +export const searchAggsSetupMock = (): AggsSetup => ({ + types: aggTypesRegistrySetupMock(), +}); + +const commonStartMock = (): AggsCommonStart => ({ + calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { + return new AggConfigs(indexPattern, configStates, { + typesRegistry: mockAggTypesRegistry(), + }); + }), + types: mockAggTypesRegistry(), +}); + +export const searchAggsStartMock = (): AggsStart => ({ + asScopedToClient: jest.fn().mockResolvedValue(commonStartMock()), +}); diff --git a/src/plugins/data/server/search/aggs/types.ts b/src/plugins/data/server/search/aggs/types.ts new file mode 100644 index 00000000000000..1b21d948b25d90 --- /dev/null +++ b/src/plugins/data/server/search/aggs/types.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { AggsCommonSetup, AggsStart as Start } from '../../../common'; + +export type AggsSetup = AggsCommonSetup; + +export interface AggsStart { + asScopedToClient: (savedObjectsClient: SavedObjectsClientContract) => Promise; +} diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index cea2714671f0b4..4a3990621ca396 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -22,3 +22,5 @@ export { ISearchStrategy, ISearchOptions, ISearchSetup, ISearchStart } from './t export { getDefaultSearchParams, getTotalLoaded } from './es_search'; export { usageProvider, SearchUsage } from './collectors'; + +export * from './aggs'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index b210df3c55db96..578a170f468bff 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,14 +17,19 @@ * under the License. */ -export function createSearchSetupMock() { +import { ISearchSetup, ISearchStart } from './types'; +import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; + +export function createSearchSetupMock(): jest.Mocked { return { + aggs: searchAggsSetupMock(), registerSearchStrategy: jest.fn(), }; } -export function createSearchStartMock() { +export function createSearchStartMock(): jest.Mocked { return { + aggs: searchAggsStartMock(), getSearchStrategy: jest.fn(), search: jest.fn(), }; diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index be00b7409fe4a9..030f37d0f7c463 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -17,15 +17,18 @@ * under the License. */ +import { CoreSetup, CoreStart } from '../../../../core/server'; import { coreMock } from '../../../../core/server/mocks'; -import { SearchService } from './search_service'; -import { CoreSetup } from '../../../../core/server'; import { DataPluginStart } from '../plugin'; +import { createFieldFormatsStartMock } from '../field_formats/mocks'; + +import { SearchService, SearchServiceSetupDependencies } from './search_service'; describe('Search service', () => { let plugin: SearchService; let mockCoreSetup: MockedKeys>; + let mockCoreStart: MockedKeys; beforeEach(() => { const mockLogger: any = { @@ -33,19 +36,27 @@ describe('Search service', () => { }; plugin = new SearchService(coreMock.createPluginInitializerContext({}), mockLogger); mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); }); describe('setup()', () => { it('exposes proper contract', async () => { - const setup = plugin.setup(mockCoreSetup, {}); + const setup = plugin.setup(mockCoreSetup, ({ + packageInfo: { version: '8' }, + registerFunction: jest.fn(), + } as unknown) as SearchServiceSetupDependencies); + expect(setup).toHaveProperty('aggs'); expect(setup).toHaveProperty('registerSearchStrategy'); }); }); describe('start()', () => { it('exposes proper contract', async () => { - const setup = plugin.start(); - expect(setup).toHaveProperty('getSearchStrategy'); + const start = plugin.start(mockCoreStart, { + fieldFormats: createFieldFormatsStartMock(), + }); + expect(start).toHaveProperty('aggs'); + expect(start).toHaveProperty('getSearchStrategy'); }); }); }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 9dc47369567af9..a8b1cdd608a845 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -18,13 +18,18 @@ */ import { + CoreSetup, + CoreStart, + Logger, Plugin, PluginInitializerContext, - CoreSetup, RequestHandlerContext, - Logger, } from '../../../../core/server'; import { ISearchSetup, ISearchStart, ISearchStrategy } from './types'; + +import { AggsService, AggsSetupDependencies } from './aggs'; + +import { FieldFormatsStart } from '../field_formats'; import { registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; import { DataPluginStart } from '../plugin'; @@ -38,7 +43,19 @@ interface StrategyMap { [name: string]: ISearchStrategy; } +/** @internal */ +export interface SearchServiceSetupDependencies { + registerFunction: AggsSetupDependencies['registerFunction']; + usageCollection?: UsageCollectionSetup; +} + +/** @internal */ +export interface SearchServiceStartDependencies { + fieldFormats: FieldFormatsStart; +} + export class SearchService implements Plugin { + private readonly aggsService = new AggsService(); private searchStrategies: StrategyMap = {}; constructor( @@ -48,7 +65,7 @@ export class SearchService implements Plugin { public setup( core: CoreSetup, - { usageCollection }: { usageCollection?: UsageCollectionSetup } + { registerFunction, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { const usage = usageCollection ? usageProvider(core) : undefined; @@ -68,7 +85,11 @@ export class SearchService implements Plugin { registerSearchRoute(core); - return { registerSearchStrategy: this.registerSearchStrategy, usage }; + return { + aggs: this.aggsService.setup({ registerFunction }), + registerSearchStrategy: this.registerSearchStrategy, + usage, + }; } private search( @@ -83,8 +104,12 @@ export class SearchService implements Plugin { ); } - public start(): ISearchStart { + public start( + { uiSettings }: CoreStart, + { fieldFormats }: SearchServiceStartDependencies + ): ISearchStart { return { + aggs: this.aggsService.start({ fieldFormats, uiSettings }), getSearchStrategy: this.getSearchStrategy, search: ( context: RequestHandlerContext, @@ -96,7 +121,9 @@ export class SearchService implements Plugin { }; } - public stop() {} + public stop() { + this.aggsService.stop(); + } private registerSearchStrategy = (name: string, strategy: ISearchStrategy) => { this.logger.info(`Register strategy ${name}`); diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 76afd7e8c951c3..fe54975d766244 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -19,6 +19,7 @@ import { RequestHandlerContext } from '../../../../core/server'; import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; +import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors/usage'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; @@ -31,6 +32,7 @@ export interface ISearchOptions { } export interface ISearchSetup { + aggs: AggsSetup; /** * Extension point exposed for other plugins to register their own search * strategies. @@ -44,6 +46,7 @@ export interface ISearchSetup { } export interface ISearchStart { + aggs: AggsStart; /** * Get other registered search strategies. For example, if a new strategy needs to use the * already-registered ES search strategy, it can use this function to accomplish that. diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 37d569a4bf9fea..9c8a79f27a9db2 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -4,7 +4,9 @@ ```ts +import { $Values } from '@kbn/utility-types'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { Assign } from '@kbn/utility-types'; import Boom from 'boom'; import { BulkIndexDocumentsParams } from 'elasticsearch'; import { CatAliasesParams } from 'elasticsearch'; @@ -22,7 +24,6 @@ import { CatTasksParams } from 'elasticsearch'; import { CatThreadPoolParams } from 'elasticsearch'; import { ClearScrollParams } from 'elasticsearch'; import { Client } from 'elasticsearch'; -import { ClientOptions } from '@elastic/elasticsearch'; import { ClusterAllocationExplainParams } from 'elasticsearch'; import { ClusterGetSettingsParams } from 'elasticsearch'; import { ClusterHealthParams } from 'elasticsearch'; @@ -31,19 +32,23 @@ import { ClusterPutSettingsParams } from 'elasticsearch'; import { ClusterRerouteParams } from 'elasticsearch'; import { ClusterStateParams } from 'elasticsearch'; import { ClusterStatsParams } from 'elasticsearch'; -import { ConfigOptions } from 'elasticsearch'; +import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; +import { CoreStart } from 'src/core/server'; import { CountParams } from 'elasticsearch'; import { CreateDocumentParams } from 'elasticsearch'; import { DeleteDocumentByQueryParams } from 'elasticsearch'; import { DeleteDocumentParams } from 'elasticsearch'; import { DeleteScriptParams } from 'elasticsearch'; import { DeleteTemplateParams } from 'elasticsearch'; -import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; +import { Ensure } from '@kbn/utility-types'; import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; +import { ExpressionAstFunction } from 'src/plugins/expressions/common'; +import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { FetchOptions } from 'src/plugins/data/public'; import { FieldStatsParams } from 'elasticsearch'; import { GenericParams } from 'elasticsearch'; import { GetParams } from 'elasticsearch'; @@ -94,13 +99,15 @@ import { IngestDeletePipelineParams } from 'elasticsearch'; import { IngestGetPipelineParams } from 'elasticsearch'; import { IngestPutPipelineParams } from 'elasticsearch'; import { IngestSimulateParams } from 'elasticsearch'; +import { ISearchSource } from 'src/plugins/data/public'; import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { KibanaConfigType as KibanaConfigType_2 } from 'src/core/server/kibana_config'; -import { KibanaRequest as KibanaRequest_2 } from 'kibana/server'; +import { KibanaRequest } from 'kibana/server'; import { LegacyAPICaller as LegacyAPICaller_2 } from 'kibana/server'; import { Logger as Logger_2 } from 'kibana/server'; import { MGetParams } from 'elasticsearch'; import { MGetResponse } from 'elasticsearch'; +import { Moment } from 'moment'; import moment from 'moment'; import { MSearchParams } from 'elasticsearch'; import { MSearchResponse } from 'elasticsearch'; @@ -109,27 +116,26 @@ import { MTermVectorsParams } from 'elasticsearch'; import { NodesHotThreadsParams } from 'elasticsearch'; import { NodesInfoParams } from 'elasticsearch'; import { NodesStatsParams } from 'elasticsearch'; -import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; -import { PeerCertificate } from 'tls'; import { PingParams } from 'elasticsearch'; +import { Plugin as Plugin_2 } from 'src/core/server'; +import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/server'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; import { RecursiveReadonly } from '@kbn/utility-types'; import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; -import { Request } from 'hapi'; -import { ResponseObject } from 'hapi'; -import { ResponseToolkit } from 'hapi'; -import { SavedObject as SavedObject_2 } from 'src/core/server'; -import { SchemaTypeError } from '@kbn/config-schema'; +import { RequestAdapter } from 'src/plugins/inspector/common'; +import { RequestStatistics } from 'src/plugins/inspector/common'; +import { SavedObject } from 'src/core/server'; +import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'src/core/server'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; import { SearchResponse } from 'elasticsearch'; import { SearchShardsParams } from 'elasticsearch'; import { SearchTemplateParams } from 'elasticsearch'; -import { ShallowPromise } from '@kbn/utility-types'; +import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; import { ShardsResponse } from 'elasticsearch'; import { SnapshotCreateParams } from 'elasticsearch'; import { SnapshotCreateRepositoryParams } from 'elasticsearch'; @@ -140,7 +146,6 @@ import { SnapshotGetRepositoryParams } from 'elasticsearch'; import { SnapshotRestoreParams } from 'elasticsearch'; import { SnapshotStatusParams } from 'elasticsearch'; import { SnapshotVerifyRepositoryParams } from 'elasticsearch'; -import { Stream } from 'stream'; import { SuggestParams } from 'elasticsearch'; import { TasksCancelParams } from 'elasticsearch'; import { TasksGetParams } from 'elasticsearch'; @@ -156,7 +161,96 @@ import { Unit } from '@elastic/datemath'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; -import { Url } from 'url'; + +// Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AggConfigOptions = Assign; + +// Warning: (ae-missing-release-tag) "AggGroupLabels" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const AggGroupLabels: { + buckets: string; + metrics: string; + none: string; +}; + +// Warning: (ae-missing-release-tag) "AggGroupName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AggGroupName = $Values; + +// Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const AggGroupNames: Readonly<{ + Buckets: "buckets"; + Metrics: "metrics"; + None: "none"; +}>; + +// Warning: (ae-forgotten-export) The symbol "BaseParamType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "AggParam" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AggParam = BaseParamType; + +// Warning: (ae-missing-release-tag) "AggParamOption" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface AggParamOption { + // (undocumented) + display: string; + // Warning: (ae-forgotten-export) The symbol "AggConfig" needs to be exported by the entry point index.d.ts + // + // (undocumented) + enabled?(agg: AggConfig): boolean; + // (undocumented) + val: string; +} + +// Warning: (ae-missing-release-tag) "AggParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class AggParamType extends BaseParamType { + constructor(config: Record); + // (undocumented) + allowedAggs: string[]; + // (undocumented) + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; +} + +// Warning: (ae-missing-release-tag) "BUCKET_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum BUCKET_TYPES { + // (undocumented) + DATE_HISTOGRAM = "date_histogram", + // (undocumented) + DATE_RANGE = "date_range", + // (undocumented) + FILTER = "filter", + // (undocumented) + FILTERS = "filters", + // (undocumented) + GEOHASH_GRID = "geohash_grid", + // (undocumented) + GEOTILE_GRID = "geotile_grid", + // (undocumented) + HISTOGRAM = "histogram", + // (undocumented) + IP_RANGE = "ip_range", + // (undocumented) + RANGE = "range", + // (undocumented) + SIGNIFICANT_TERMS = "significant_terms", + // (undocumented) + TERMS = "terms" +} // Warning: (ae-missing-release-tag) "castEsToKbnFieldTypeName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -369,6 +463,22 @@ export function getTotalLoaded({ total, failed, successful }: ShardsResponse): { loaded: number; }; +// Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type IAggConfig = AggConfig; + +// Warning: (ae-forgotten-export) The symbol "AggConfigs" needs to be exported by the entry point index.d.ts +// +// @internal +export type IAggConfigs = AggConfigs; + +// Warning: (ae-forgotten-export) The symbol "AggType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type IAggType = AggType; + // Warning: (ae-forgotten-export) The symbol "IKibanaSearchRequest" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -398,6 +508,12 @@ export interface IEsSearchResponse extends IKibanaSearchResponse { // @public (undocumented) export type IFieldFormatsRegistry = PublicMethodsOf; +// Warning: (ae-forgotten-export) The symbol "FieldParamType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IFieldParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type IFieldParamType = FieldParamType; + // Warning: (ae-missing-release-tag) "IFieldSubType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -479,6 +595,12 @@ export interface IIndexPattern { type?: string; } +// Warning: (ae-forgotten-export) The symbol "MetricAggType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IMetricAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type IMetricAggType = MetricAggType; + // Warning: (ae-missing-release-tag) "IndexPatternAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated @@ -561,6 +683,10 @@ export interface ISearchOptions { // // @public (undocumented) export interface ISearchSetup { + // Warning: (ae-forgotten-export) The symbol "AggsSetup" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggs: AggsSetup; registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; usage?: SearchUsage; } @@ -569,6 +695,10 @@ export interface ISearchSetup { // // @public (undocumented) export interface ISearchStart { + // Warning: (ae-forgotten-export) The symbol "AggsStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggs: AggsStart; getSearchStrategy: (name: string) => ISearchStrategy; // Warning: (ae-forgotten-export) The symbol "RequestHandlerContext" needs to be exported by the entry point index.d.ts // @@ -632,6 +762,77 @@ export interface KueryNode { type: keyof NodeTypes; } +// Warning: (ae-missing-release-tag) "METRIC_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum METRIC_TYPES { + // (undocumented) + AVG = "avg", + // (undocumented) + AVG_BUCKET = "avg_bucket", + // (undocumented) + CARDINALITY = "cardinality", + // (undocumented) + COUNT = "count", + // (undocumented) + CUMULATIVE_SUM = "cumulative_sum", + // (undocumented) + DERIVATIVE = "derivative", + // (undocumented) + GEO_BOUNDS = "geo_bounds", + // (undocumented) + GEO_CENTROID = "geo_centroid", + // (undocumented) + MAX = "max", + // (undocumented) + MAX_BUCKET = "max_bucket", + // (undocumented) + MEDIAN = "median", + // (undocumented) + MIN = "min", + // (undocumented) + MIN_BUCKET = "min_bucket", + // (undocumented) + MOVING_FN = "moving_avg", + // (undocumented) + PERCENTILE_RANKS = "percentile_ranks", + // (undocumented) + PERCENTILES = "percentiles", + // (undocumented) + SERIAL_DIFF = "serial_diff", + // (undocumented) + STD_DEV = "std_dev", + // (undocumented) + SUM = "sum", + // (undocumented) + SUM_BUCKET = "sum_bucket", + // (undocumented) + TOP_HITS = "top_hits" +} + +// Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class OptionedParamType extends BaseParamType { + constructor(config: Record); + // (undocumented) + options: OptionedValueProp[]; +} + +// Warning: (ae-missing-release-tag) "OptionedValueProp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface OptionedValueProp { + // (undocumented) + disabled?: boolean; + // (undocumented) + isCompatible: (agg: IAggConfig) => boolean; + // (undocumented) + text: string; + // (undocumented) + value: string; +} + // Warning: (ae-forgotten-export) The symbol "parseEsInterval" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ParsedInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -643,25 +844,20 @@ export type ParsedInterval = ReturnType; // @public (undocumented) export function parseInterval(interval: string): moment.Duration | null; -// Warning: (ae-forgotten-export) The symbol "Plugin" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataPluginSetupDependencies" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DataServerPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Plugin implements Plugin_2 { - // Warning: (ae-forgotten-export) The symbol "PluginInitializerContext" needs to be exported by the entry point index.d.ts - constructor(initializerContext: PluginInitializerContext); - // Warning: (ae-forgotten-export) The symbol "CoreSetup" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "DataPluginSetupDependencies" needs to be exported by the entry point index.d.ts - // +export class Plugin implements Plugin_2 { + constructor(initializerContext: PluginInitializerContext_2); // (undocumented) - setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { + setup(core: CoreSetup, { expressions, usageCollection }: DataPluginSetupDependencies): { search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; }; - // Warning: (ae-forgotten-export) The symbol "CoreStart" needs to be exported by the entry point index.d.ts - // // (undocumented) start(core: CoreStart): { search: ISearchStart; @@ -676,6 +872,8 @@ export class Plugin implements Plugin_2 { stop(): void; } +// Warning: (ae-forgotten-export) The symbol "PluginInitializerContext" needs to be exported by the entry point index.d.ts +// // @public export function plugin(initializerContext: PluginInitializerContext): Plugin; @@ -734,16 +932,36 @@ export interface RefreshInterval { // @public (undocumented) export const search: { aggs: { + CidrMask: typeof CidrMask; dateHistogramInterval: typeof dateHistogramInterval; + intervalOptions: ({ + display: string; + val: string; + enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + } | { + display: string; + val: string; + })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; Ipv4Address: typeof Ipv4Address; + isNumberType: (agg: import("../common").AggConfig) => boolean; + isStringType: (agg: import("../common").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("../common").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; + parentPipelineType: string; parseEsInterval: typeof parseEsInterval; parseInterval: typeof parseInterval; + propFilter: typeof propFilter; + siblingPipelineType: string; + termsAggFilter: string[]; toAbsoluteDates: typeof toAbsoluteDates; }; + getRequestInspectorStats: typeof getRequestInspectorStats; + getResponseInspectorStats: typeof getResponseInspectorStats; + tabifyAggResponse: typeof tabifyAggResponse; + tabifyGetColumns: typeof tabifyGetColumns; }; // Warning: (ae-missing-release-tag) "SearchUsage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -761,6 +979,27 @@ export interface SearchUsage { // @public (undocumented) export function shouldReadFieldFromDocValues(aggregatable: boolean, esType: string): boolean; +// @public (undocumented) +export interface TabbedAggColumn { + // (undocumented) + aggConfig: IAggConfig; + // (undocumented) + id: string; + // (undocumented) + name: string; +} + +// @public (undocumented) +export type TabbedAggRow = Record; + +// @public (undocumented) +export interface TabbedTable { + // (undocumented) + columns: TabbedAggColumn[]; + // (undocumented) + rows: TabbedAggRow[]; +} + // Warning: (ae-missing-release-tag) "TimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -836,13 +1075,19 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:189:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:190:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:193:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:221:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:221:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:221:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:221:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:223:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:224:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:233:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:234:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:235:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index 1a094a36f68e3d..ebf8e09e863969 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -10,7 +10,6 @@ import { dateHistogramOperation } from './index'; import { shallow } from 'enzyme'; import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; -import { coreMock } from 'src/core/public/mocks'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; import { @@ -21,14 +20,13 @@ import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; const dataStart = dataPluginMock.createStartContract(); -dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression({ - ...coreMock.createStart().uiSettings, - get: (path: string) => { +dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression( + (path: string) => { if (path === UI_SETTINGS.HISTOGRAM_MAX_BARS) { return 10; } - }, -} as IUiSettingsClient); + } +); const defaultOptions = { storage: {} as IStorageWrapper, From ee9a8d29b1588e271486c428e423880f41fa8ee9 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Thu, 13 Aug 2020 23:44:17 +0300 Subject: [PATCH 06/46] [I18n] verify select icu-message options are in english (#74963) Co-authored-by: Elastic Machine --- src/dev/i18n/index.ts | 1 - src/dev/i18n/integrate_locale_files.test.ts | 1 - src/dev/i18n/integrate_locale_files.ts | 3 +- .../__snapshots__/utils.test.js.snap | 2 +- src/dev/i18n/utils/index.ts | 48 ++++++++++ src/dev/i18n/utils/intl_types.ts | 41 +++++++++ src/dev/i18n/{ => utils}/utils.js | 22 ----- src/dev/i18n/{ => utils}/utils.test.js | 0 src/dev/i18n/utils/verify_icu_message.test.ts | 91 +++++++++++++++++++ src/dev/i18n/utils/verify_icu_message.ts | 74 +++++++++++++++ src/dev/run_i18n_check.ts | 16 ++-- .../translations/translations/ja-JP.json | 1 - 12 files changed, 266 insertions(+), 34 deletions(-) rename src/dev/i18n/{ => utils}/__snapshots__/utils.test.js.snap (95%) create mode 100644 src/dev/i18n/utils/index.ts create mode 100644 src/dev/i18n/utils/intl_types.ts rename src/dev/i18n/{ => utils}/utils.js (95%) rename src/dev/i18n/{ => utils}/utils.test.js (100%) create mode 100644 src/dev/i18n/utils/verify_icu_message.test.ts create mode 100644 src/dev/i18n/utils/verify_icu_message.ts diff --git a/src/dev/i18n/index.ts b/src/dev/i18n/index.ts index cfc03f1c08b3c2..68e6ab16460927 100644 --- a/src/dev/i18n/index.ts +++ b/src/dev/i18n/index.ts @@ -21,7 +21,6 @@ export { extractMessagesFromPathToMap } from './extract_default_translations'; // @ts-ignore export { matchEntriesWithExctractors } from './extract_default_translations'; -// @ts-ignore export { arrayify, writeFileAsync, readFileAsync, normalizePath, ErrorReporter } from './utils'; export { serializeToJson, serializeToJson5 } from './serializers'; export { diff --git a/src/dev/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 3bd3dc61c044f4..24c24682b28744 100644 --- a/src/dev/i18n/integrate_locale_files.test.ts +++ b/src/dev/i18n/integrate_locale_files.test.ts @@ -21,7 +21,6 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.t import path from 'path'; import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files'; -// @ts-expect-error import { normalizePath } from './utils'; const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index f9cd6dd1971c75..ed4f7db4376bb0 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -32,7 +32,6 @@ import { readFileAsync, writeFileAsync, verifyICUMessage, - // @ts-expect-error } from './utils'; import { I18nConfig } from './config'; @@ -112,7 +111,7 @@ export function verifyMessages( if (defaultMessage) { try { const message = localizedMessagesMap.get(messageId)!; - verifyICUMessage(message); + verifyICUMessage(typeof message === 'string' ? message : message?.text); } catch (err) { if (options.ignoreMalformed) { localizedMessagesMap.delete(messageId); diff --git a/src/dev/i18n/__snapshots__/utils.test.js.snap b/src/dev/i18n/utils/__snapshots__/utils.test.js.snap similarity index 95% rename from src/dev/i18n/__snapshots__/utils.test.js.snap rename to src/dev/i18n/utils/__snapshots__/utils.test.js.snap index b4e15304cca968..147ef5987c1dcc 100644 --- a/src/dev/i18n/__snapshots__/utils.test.js.snap +++ b/src/dev/i18n/utils/__snapshots__/utils.test.js.snap @@ -9,7 +9,7 @@ exports[`i18n utils should create verbose parser error message 1`] = ` " `; -exports[`i18n utils should normalizePath 1`] = `"src/dev/i18n"`; +exports[`i18n utils should normalizePath 1`] = `"src/dev/i18n/utils"`; exports[`i18n utils should not escape linebreaks 1`] = ` "Text diff --git a/src/dev/i18n/utils/index.ts b/src/dev/i18n/utils/index.ts new file mode 100644 index 00000000000000..3f81ab2ca66c30 --- /dev/null +++ b/src/dev/i18n/utils/index.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + // constants + readFileAsync, + writeFileAsync, + makeDirAsync, + accessAsync, + globAsync, + // functions + normalizePath, + difference, + isPropertyWithKey, + isI18nTranslateFunction, + node, + formatJSString, + formatHTMLString, + traverseNodes, + createParserErrorMessage, + checkValuesProperty, + extractValueReferencesFromMessage, + extractMessageIdFromNode, + extractMessageValueFromNode, + extractDescriptionValueFromNode, + extractValuesKeysFromNode, + arrayify, + // classes + ErrorReporter, // @ts-ignore +} from './utils'; + +export { verifyICUMessage } from './verify_icu_message'; diff --git a/src/dev/i18n/utils/intl_types.ts b/src/dev/i18n/utils/intl_types.ts new file mode 100644 index 00000000000000..adbceaea665403 --- /dev/null +++ b/src/dev/i18n/utils/intl_types.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface OptionalFormatPatternNode { + type: 'optionalFormatPattern'; + selector: string; + value: any; +} + +export interface LinePosition { + offset: number; + line: number; + column: number; +} + +export interface LocationNode { + start: LinePosition; + end: LinePosition; +} + +export interface SelectFormatNode { + type: 'selectFormat'; + options: OptionalFormatPatternNode[]; + location: LocationNode; +} diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils/utils.js similarity index 95% rename from src/dev/i18n/utils.js rename to src/dev/i18n/utils/utils.js index 11a002fdbf4a86..1d1c3118e08526 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils/utils.js @@ -208,28 +208,6 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI } } -/** - * Verifies valid ICU message. - * @param message ICU message. - * @param messageId ICU message id - * @returns {undefined} - */ -export function verifyICUMessage(message) { - try { - parser.parse(message); - } catch (error) { - if (error.name === 'SyntaxError') { - const errorWithContext = createParserErrorMessage(message, { - loc: { - line: error.location.start.line, - column: error.location.start.column - 1, - }, - message: error.message, - }); - throw errorWithContext; - } - } -} /** * Extracts value references from the ICU message. * @param message ICU message. diff --git a/src/dev/i18n/utils.test.js b/src/dev/i18n/utils/utils.test.js similarity index 100% rename from src/dev/i18n/utils.test.js rename to src/dev/i18n/utils/utils.test.js diff --git a/src/dev/i18n/utils/verify_icu_message.test.ts b/src/dev/i18n/utils/verify_icu_message.test.ts new file mode 100644 index 00000000000000..0a4510352c4294 --- /dev/null +++ b/src/dev/i18n/utils/verify_icu_message.test.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { verifyICUMessage, checkEnglishOnly } from './verify_icu_message'; + +describe('verifyICUMessage', () => { + it('passes on plain text', () => { + const message = 'plain text here'; + expect(() => verifyICUMessage(message)).not.toThrowError(); + }); + + it('passes on empty string', () => { + const message = ''; + expect(() => verifyICUMessage(message)).not.toThrowError(); + }); + + it('passes on variable icu-syntax', () => { + const message = 'Your regular {foobar}'; + expect(() => verifyICUMessage(message)).not.toThrowError(); + }); + + it('passes on correct plural icu-syntax', () => { + const message = `You have {itemCount, plural, + =0 {no items} + one {1 item} + other {{itemCount} items} + }.`; + + expect(() => verifyICUMessage(message)).not.toThrowError(); + }); + + it('throws on malformed string', () => { + const message = + 'CDATA[extended_bounds設定を使用すると、強制的にヒストグラムアグリゲーションを実行し、特定の最小値に対してバケットの作成を開始し、最大値までバケットを作成し続けます。 ]]>\n\t\t\tKibana-SW - String "data.search.aggs.buckets.dateHistogram.extendedBounds.help" in Json.Root "messages\\strings" '; + + expect(() => verifyICUMessage(message)).toThrowError(); + }); + + it('throws on missing curly brackets', () => { + const message = `A missing {curly`; + + expect(() => verifyICUMessage(message)).toThrowError(); + }); + + it('throws on incorrect plural icu-syntax', () => { + // Notice that small/Medium/Large constants are swapped with the translation strings. + const message = + '{textScale, select, small {小さい} 中くらい {Medium} 大きい {Large} その他の {{textScale}} }'; + + expect(() => verifyICUMessage(message)).toThrowError(); + }); +}); + +describe('checkEnglishOnly', () => { + it('returns true on english only message', () => { + const result = checkEnglishOnly('english'); + + expect(result).toEqual(true); + }); + it('returns true on empty message', () => { + const result = checkEnglishOnly(''); + + expect(result).toEqual(true); + }); + it('returns false on message containing numbers', () => { + const result = checkEnglishOnly('english 123'); + + expect(result).toEqual(false); + }); + it('returns false on message containing non-english alphabets', () => { + const result = checkEnglishOnly('i am 大きい'); + + expect(result).toEqual(false); + }); +}); diff --git a/src/dev/i18n/utils/verify_icu_message.ts b/src/dev/i18n/utils/verify_icu_message.ts new file mode 100644 index 00000000000000..683d91c5c4939c --- /dev/null +++ b/src/dev/i18n/utils/verify_icu_message.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import parser from 'intl-messageformat-parser'; +// @ts-ignore +import { createParserErrorMessage, traverseNodes } from './utils'; +import { SelectFormatNode } from './intl_types'; + +export function checkEnglishOnly(message: string) { + return /^[a-z]*$/i.test(message); +} + +export function verifySelectFormatNode(node: SelectFormatNode) { + if (node.type !== 'selectFormat') { + throw new parser.SyntaxError( + 'Unable to verify select format icu-syntax', + 'selectFormat', + node.type, + node.location + ); + } + + for (const option of node.options) { + if (option.type === 'optionalFormatPattern') { + if (!checkEnglishOnly(option.selector)) { + throw new parser.SyntaxError( + 'selectFormat Selector must be in english', + 'English only selector', + option.selector, + node.location + ); + } + } + } +} + +export function verifyICUMessage(message: string) { + try { + const results = parser.parse(message); + for (const node of results.elements) { + if (node.type === 'argumentElement' && node.format?.type === 'selectFormat') { + verifySelectFormatNode(node.format); + } + } + } catch (error) { + if (error.name === 'SyntaxError') { + const errorWithContext = createParserErrorMessage(message, { + loc: { + line: error.location.start.line, + column: error.location.start.column - 1, + }, + message: error.message, + }); + throw errorWithContext; + } + } +} diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 70eeedac2b8b6a..17f3fd5d1c7349 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -30,7 +30,8 @@ import { mergeConfigs, } from './i18n/tasks'; -const skipNoTranslations = ({ config }: { config: I18nConfig }) => !config.translations.length; +const skipOnNoTranslations = ({ config }: { config: I18nConfig }) => + !config.translations.length && 'No translations found.'; run( async ({ @@ -40,6 +41,7 @@ run( 'ignore-missing': ignoreMissing, 'ignore-unused': ignoreUnused, 'include-config': includeConfig, + 'ignore-untracked': ignoreUntracked, fix = false, path, }, @@ -50,12 +52,13 @@ run( (ignoreIncompatible !== undefined || ignoreUnused !== undefined || ignoreMalformed !== undefined || - ignoreMissing !== undefined) + ignoreMissing !== undefined || + ignoreUntracked !== undefined) ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing is allowed when --fix is set.` + )} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing, --ignore-untracked is allowed when --fix is set.` ); } @@ -83,19 +86,20 @@ run( }, { title: 'Checking For Untracked Messages based on .i18nrc.json', - skip: skipNoTranslations, + enabled: (_) => !ignoreUntracked, + skip: skipOnNoTranslations, task: ({ config }) => new Listr(extractUntrackedMessages(srcPaths), { exitOnError: true }), }, { title: 'Validating Default Messages', - skip: skipNoTranslations, + skip: skipOnNoTranslations, task: ({ config }) => new Listr(extractDefaultMessages(config, srcPaths), { exitOnError: true }), }, { title: 'Compatibility Checks', - skip: skipNoTranslations, + skip: skipOnNoTranslations, task: ({ config }) => new Listr( checkCompatibility( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 81c06cf5c381f6..526f2d88574d34 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4852,7 +4852,6 @@ "xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "非ヒープ領域の平均", "xpack.apm.jvmsTable.threadCountColumnLabel": "最大スレッド数", "xpack.apm.kueryBar.disabledPlaceholder": "ここでは検索は利用できません", - "xpack.apm.kueryBar.placeholder": "検索 {event, select,\n トランザクション {transactions}\n メトリック: {metric}\n エラー {errors}\n その他 {transactions, errors and metrics}\n } (E.g. {queryExample})", "xpack.apm.license.betaBadge": "ベータ", "xpack.apm.license.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.license.button": "トライアルを開始", From a8ed1f4b16a0be6879f4b212df0eae6890acba75 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 13 Aug 2020 14:33:27 -0700 Subject: [PATCH 07/46] [Reporting] Update more Server Types for TaskManager (#74915) * [Reporting] Update more Server Types for TaskManager * remove some task manager references * more strict * more strict 2 * simplify * fix test * fix test * routing validation unused types cleanup * remove more casting in route handlers * feedback changes * original comment was fine Co-authored-by: Elastic Machine --- x-pack/plugins/reporting/server/core.ts | 8 +- .../server/export_types/csv/create_job.ts | 4 +- .../export_types/csv/execute_job.test.ts | 112 +++---- .../server/export_types/csv/execute_job.ts | 4 +- .../export_types/csv/generate_csv/index.ts | 4 + .../server/export_types/csv/index.ts | 8 +- .../server/export_types/csv/types.d.ts | 6 +- .../csv_from_savedobject/execute_job.ts | 2 +- .../export_types/png/create_job/index.ts | 4 +- .../export_types/png/execute_job/index.ts | 4 +- .../server/export_types/png/index.ts | 8 +- .../server/export_types/png/types.d.ts | 7 +- .../printable_pdf/create_job/index.ts | 6 +- .../printable_pdf/execute_job/index.ts | 4 +- .../export_types/printable_pdf/index.ts | 8 +- .../export_types/printable_pdf/types.d.ts | 6 +- .../reporting/server/lib/create_worker.ts | 6 +- .../reporting/server/lib/enqueue_job.ts | 38 ++- .../server/lib/esqueue/constants/index.js | 4 +- .../reporting/server/lib/esqueue/worker.js | 48 +-- x-pack/plugins/reporting/server/lib/index.ts | 3 +- .../lib/{esqueue/constants => }/statuses.ts | 0 .../reporting/server/lib/store/mapping.ts | 2 +- .../reporting/server/lib/store/report.test.ts | 113 +++++-- .../reporting/server/lib/store/report.ts | 144 +++++++-- .../reporting/server/lib/store/store.test.ts | 304 ++++++++++++++---- .../reporting/server/lib/store/store.ts | 194 +++++++---- x-pack/plugins/reporting/server/plugin.ts | 10 +- .../server/routes/generate_from_jobparams.ts | 15 +- .../generate_from_savedobject_immediate.ts | 10 +- .../server/routes/generation.test.ts | 3 +- .../reporting/server/routes/generation.ts | 10 +- .../server/routes/lib/get_document_payload.ts | 7 +- .../routes/lib/get_job_params_from_request.ts | 16 +- .../reporting/server/routes/types.d.ts | 4 +- .../create_mock_reportingplugin.ts | 19 +- x-pack/plugins/reporting/server/types.ts | 44 +-- 37 files changed, 774 insertions(+), 415 deletions(-) rename x-pack/plugins/reporting/server/lib/{esqueue/constants => }/statuses.ts (100%) diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 95dc7586ad4a6b..25594e1c0140b7 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -23,7 +23,6 @@ import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factor import { screenshotsObservableFactory } from './lib/screenshots'; import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; -import { EnqueueJobFn } from './lib/enqueue_job'; import { ReportingStore } from './lib/store'; export interface ReportingInternalSetup { @@ -36,7 +35,6 @@ export interface ReportingInternalSetup { export interface ReportingInternalStart { browserDriverFactory: HeadlessChromiumDriverFactory; - enqueueJob: EnqueueJobFn; esqueue: ESQueueInstance; store: ReportingStore; savedObjects: SavedObjectsServiceStart; @@ -115,7 +113,7 @@ export class ReportingCore { /* * Gives async access to the startDeps */ - private async getPluginStartDeps() { + public async getPluginStartDeps() { if (this.pluginStartDeps) { return this.pluginStartDeps; } @@ -131,10 +129,6 @@ export class ReportingCore { return (await this.getPluginStartDeps()).esqueue; } - public async getEnqueueJob() { - return (await this.getPluginStartDeps()).enqueueJob; - } - public async getLicenseInfo() { const { licensing } = this.getPluginSetupDeps(); return await licensing.license$ diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 5e8ce923a79e0b..252968e386b53f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -5,10 +5,10 @@ */ import { cryptoFactory } from '../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../types'; +import { CreateJobFn, ScheduleTaskFnFactory } from '../../types'; import { JobParamsDiscoverCsv } from './types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 75070c06824e2d..5eeef0f9906dd4 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -5,7 +5,7 @@ */ import nodeCrypto from '@elastic/node-crypto'; -import { IUiSettingsClient, ElasticsearchServiceSetup } from 'kibana/server'; +import { ElasticsearchServiceSetup, IUiSettingsClient } from 'kibana/server'; // @ts-ignore import Puid from 'puid'; import sinon from 'sinon'; @@ -20,8 +20,8 @@ import { CSV_BOM_CHARS } from '../../../common/constants'; import { LevelLogger } from '../../lib'; import { setFieldFormats } from '../../services'; import { createMockReportingCore } from '../../test_helpers'; -import { ScheduledTaskParamsCSV } from './types'; import { runTaskFnFactory } from './execute_job'; +import { ScheduledTaskParamsCSV } from './types'; const delay = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(), ms)); @@ -125,7 +125,7 @@ describe('CSV Execute Job', function () { describe('basic Elasticsearch call behavior', function () { it('should decrypt encrypted headers and pass to callAsCurrentUser', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', getScheduledTaskParams({ @@ -145,7 +145,7 @@ describe('CSV Execute Job', function () { testBody: true, }; - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const job = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], @@ -172,7 +172,7 @@ describe('CSV Execute Job', function () { _scroll_id: scrollId, }); callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', getScheduledTaskParams({ @@ -190,7 +190,7 @@ describe('CSV Execute Job', function () { }); it('should not execute scroll if there are no hits from the search', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', getScheduledTaskParams({ @@ -224,7 +224,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', getScheduledTaskParams({ @@ -263,7 +263,7 @@ describe('CSV Execute Job', function () { _scroll_id: lastScrollId, }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', getScheduledTaskParams({ @@ -295,7 +295,7 @@ describe('CSV Execute Job', function () { _scroll_id: lastScrollId, }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -322,7 +322,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -347,7 +347,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -373,7 +373,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -399,7 +399,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -425,7 +425,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -452,7 +452,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -473,7 +473,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -496,7 +496,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -517,7 +517,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -533,7 +533,7 @@ describe('CSV Execute Job', function () { describe('Elasticsearch call errors', function () { it('should reject Promise if search call errors out', async function () { callAsCurrentUserStub.rejects(new Error()); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], @@ -552,7 +552,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); callAsCurrentUserStub.onSecondCall().rejects(new Error()); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], @@ -573,7 +573,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], @@ -592,7 +592,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], @@ -618,7 +618,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], @@ -644,7 +644,7 @@ describe('CSV Execute Job', function () { _scroll_id: undefined, }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: [], @@ -678,7 +678,7 @@ describe('CSV Execute Job', function () { }); it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', getScheduledTaskParams({ @@ -697,7 +697,7 @@ describe('CSV Execute Job', function () { }); it(`shouldn't call clearScroll if it never got a scrollId`, async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', getScheduledTaskParams({ @@ -715,7 +715,7 @@ describe('CSV Execute Job', function () { }); it('should call clearScroll if it got a scrollId', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', getScheduledTaskParams({ @@ -737,7 +737,7 @@ describe('CSV Execute Job', function () { describe('csv content', function () { it('should write column headers to output, even if there are no results', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -749,7 +749,7 @@ describe('CSV Execute Job', function () { it('should use custom uiSettings csv:separator for header', async function () { mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(';'); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -761,7 +761,7 @@ describe('CSV Execute Job', function () { it('should escape column headers if uiSettings csv:quoteValues is true', async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -773,7 +773,7 @@ describe('CSV Execute Job', function () { it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(false); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -784,7 +784,7 @@ describe('CSV Execute Job', function () { }); it('should write column headers to output, when there are results', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ one: '1', two: '2' }], @@ -798,13 +798,14 @@ describe('CSV Execute Job', function () { searchRequest: { index: null, body: null }, }); const { content } = await runTask('job123', jobParams, cancellationToken); - const lines = content.split('\n'); + expect(content).not.toBe(null); + const lines = content!.split('\n'); const headerLine = lines[0]; expect(headerLine).toBe('one,two'); }); it('should use comma separated values of non-nested fields from _source', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -819,13 +820,14 @@ describe('CSV Execute Job', function () { searchRequest: { index: null, body: null }, }); const { content } = await runTask('job123', jobParams, cancellationToken); - const lines = content.split('\n'); + expect(content).not.toBe(null); + const lines = content!.split('\n'); const valuesLine = lines[1]; expect(valuesLine).toBe('foo,bar'); }); it('should concatenate the hits from multiple responses', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -846,14 +848,15 @@ describe('CSV Execute Job', function () { searchRequest: { index: null, body: null }, }); const { content } = await runTask('job123', jobParams, cancellationToken); - const lines = content.split('\n'); + expect(content).not.toBe(null); + const lines = content!.split('\n'); expect(lines[1]).toBe('foo,bar'); expect(lines[2]).toBe('baz,qux'); }); it('should use field formatters to format fields', async function () { - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -877,7 +880,8 @@ describe('CSV Execute Job', function () { }, }); const { content } = await runTask('job123', jobParams, cancellationToken); - const lines = content.split('\n'); + expect(content).not.toBe(null); + const lines = content!.split('\n'); expect(lines[1]).toBe('FOO,bar'); }); @@ -889,13 +893,13 @@ describe('CSV Execute Job', function () { // tests use these 'simple' characters to make the math easier describe('when only the headers exceed the maxSizeBytes', function () { - let content: string; - let maxSizeReached: boolean; + let content: string | null; + let maxSizeReached: boolean | undefined; beforeEach(async function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -919,13 +923,13 @@ describe('CSV Execute Job', function () { }); describe('when headers are equal to maxSizeBytes', function () { - let content: string; - let maxSizeReached: boolean; + let content: string | null; + let maxSizeReached: boolean | undefined; beforeEach(async function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -949,8 +953,8 @@ describe('CSV Execute Job', function () { }); describe('when the data exceeds the maxSizeBytes', function () { - let content: string; - let maxSizeReached: boolean; + let content: string | null; + let maxSizeReached: boolean | undefined; beforeEach(async function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); @@ -962,7 +966,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -987,8 +991,8 @@ describe('CSV Execute Job', function () { }); describe('when headers and data equal the maxSizeBytes', function () { - let content: string; - let maxSizeReached: boolean; + let content: string | null; + let maxSizeReached: boolean | undefined; beforeEach(async function () { mockReportingCore.getUiSettingsServiceFactory = () => @@ -1002,7 +1006,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -1039,7 +1043,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -1065,7 +1069,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], @@ -1091,7 +1095,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const runTask = await runTaskFnFactory(mockReportingCore, mockLogger); + const runTask = runTaskFnFactory(mockReportingCore, mockLogger); const jobParams = getScheduledTaskParams({ headers: encryptedHeaders, fields: ['one', 'two'], diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index f0c41a6a49703d..802f4a81777c5e 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -10,7 +10,7 @@ import Hapi from 'hapi'; import { KibanaRequest } from '../../../../../../src/core/server'; import { CONTENT_TYPE_CSV, CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory, LevelLogger } from '../../lib'; -import { ESQueueWorkerExecuteFn, RunTaskFnFactory } from '../../types'; +import { WorkerExecuteFn, RunTaskFnFactory } from '../../types'; import { ScheduledTaskParamsCSV } from './types'; import { createGenerateCsv } from './generate_csv'; @@ -54,7 +54,7 @@ const getRequest = async (headers: string | undefined, crypto: Crypto, logger: L } as Hapi.Request); }; -export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index 8da27100ac31cf..06aa2434afc3f4 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -113,6 +113,10 @@ export function createGenerateCsv(logger: LevelLogger) { break; } + if (cancellationToken.isCancelled()) { + break; + } + const flattened = flattenHit(hit); const rows = formatCsvValues(flattened); const rowsHaveFormulas = diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index dffc874831dc23..4bca42e0661e5c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -13,17 +13,17 @@ import { LICENSE_TYPE_TRIAL, } from '../../../common/constants'; import { CSV_JOB_TYPE as jobType } from '../../../constants'; -import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { metadata } from './metadata'; +import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { scheduleTaskFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; import { JobParamsDiscoverCsv, ScheduledTaskParamsCSV } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsDiscoverCsv, - ESQueueCreateJobFn, + CreateJobFn, ScheduledTaskParamsCSV, - ESQueueWorkerExecuteFn + WorkerExecuteFn > => ({ ...metadata, jobType, diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index 9e86a5bb254a31..e0d09d04a3d3a4 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ScheduledTaskParams } from '../../types'; +import { CreateJobBaseParams, ScheduledTaskParams } from '../../types'; export type RawValue = string | object | null | undefined; @@ -28,10 +28,8 @@ export interface IndexPatternSavedObject { }; } -export interface JobParamsDiscoverCsv { - browserTimezone: string; +export interface JobParamsDiscoverCsv extends CreateJobBaseParams { indexPatternId: string; - objectType: string; title: string; searchRequest: SearchRequest; fields: string[]; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index 0cc9ec16ed71bc..ec7e0a21f0498a 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -41,7 +41,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e // jobID is only for "queued" jobs // Use the jobID as a logging tag or "immediate" const { jobParams } = jobPayload; - const jobLogger = logger.clone([jobId === null ? 'immediate' : jobId]); + const jobLogger = logger.clone(['immediate']); const generateCsv = createGenerateCsv(jobLogger); const { panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index 9227354520b6eb..2252177e980850 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -5,11 +5,11 @@ */ import { cryptoFactory } from '../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { CreateJobFn, ScheduleTaskFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPNG } from '../types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 9c7134736f4f61..35cd4139df413f 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; -import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../..//types'; +import { WorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../..//types'; import { decryptJobHeaders, getConditionalHeaders, @@ -18,7 +18,7 @@ import { import { generatePngObservableFactory } from '../lib/generate_png'; import { ScheduledTaskParamsPNG } from '../types'; -type QueuedPngExecutorFactory = RunTaskFnFactory>; +type QueuedPngExecutorFactory = RunTaskFnFactory>; export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFactoryFn( reporting, diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts index 25b4dbd60535b4..c966dedb6b076d 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -12,17 +12,17 @@ import { LICENSE_TYPE_TRIAL, PNG_JOB_TYPE as jobType, } from '../../../common/constants'; -import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { metadata } from './metadata'; +import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { scheduleTaskFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; import { JobParamsPNG, ScheduledTaskParamsPNG } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPNG, - ESQueueCreateJobFn, + CreateJobFn, ScheduledTaskParamsPNG, - ESQueueWorkerExecuteFn + WorkerExecuteFn > => ({ ...metadata, jobType, diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts index 4c40f55f0f0d69..1ddee8419df309 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -4,16 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ScheduledTaskParams } from '../../../server/types'; +import { CreateJobBaseParams, ScheduledTaskParams } from '../../../server/types'; import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data -export interface JobParamsPNG { - objectType: string; +export interface JobParamsPNG extends CreateJobBaseParams { title: string; relativeUrl: string; - browserTimezone: string; - layout: LayoutInstance; } // Job payload: structure of stored job data provided by create_job diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 4540983129ebcf..5de089a13bfa44 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateUrls } from '../../common'; import { cryptoFactory } from '../../../lib'; -import { ESQueueCreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { CreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { validateUrls } from '../../common'; import { JobParamsPDF } from '../types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index eb15c0a71ca3f2..5ace1c987adb5a 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PDF_JOB_TYPE } from '../../../../common/constants'; -import { ESQueueWorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; +import { WorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -19,7 +19,7 @@ import { import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { ScheduledTaskParamsPDF } from '../types'; -type QueuedPdfExecutorFactory = RunTaskFnFactory>; +type QueuedPdfExecutorFactory = RunTaskFnFactory>; export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn( reporting, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts index e5115c243c6972..7f21d36c4b72c4 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -12,17 +12,17 @@ import { LICENSE_TYPE_TRIAL, PDF_JOB_TYPE as jobType, } from '../../../common/constants'; -import { ESQueueCreateJobFn, ESQueueWorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { metadata } from './metadata'; +import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; import { scheduleTaskFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; import { JobParamsPDF, ScheduledTaskParamsPDF } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPDF, - ESQueueCreateJobFn, + CreateJobFn, ScheduledTaskParamsPDF, - ESQueueWorkerExecuteFn + WorkerExecuteFn > => ({ ...metadata, jobType, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index cba0f41f075367..7830f87780c2eb 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ScheduledTaskParams } from '../../../server/types'; +import { CreateJobBaseParams, ScheduledTaskParams } from '../../../server/types'; import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data, after being parsed from RISON -export interface JobParamsPDF { - objectType: string; // visualization, dashboard, etc. Used for job info & telemetry +export interface JobParamsPDF extends CreateJobBaseParams { title: string; relativeUrls: string[]; - browserTimezone: string; layout: LayoutInstance; } diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index 837be1f44a0930..5b0f1ddb2f1579 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../common'; import { PLUGIN_ID } from '../../common/constants'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; -import { ESQueueWorkerExecuteFn, ExportTypeDefinition, JobSource } from '../../server/types'; +import { ExportTypeDefinition, JobSource, WorkerExecuteFn } from '../../server/types'; import { ESQueueInstance } from './create_queue'; // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; @@ -22,10 +22,10 @@ export function createWorkerFactory(reporting: ReportingCore, log // Once more document types are added, this will need to be passed in return async function createWorker(queue: ESQueueInstance) { // export type / execute job map - const jobExecutors: Map> = new Map(); + const jobExecutors: Map> = new Map(); for (const exportType of reporting.getExportTypesRegistry().getAll() as Array< - ExportTypeDefinition> + ExportTypeDefinition> >) { const jobExecutor = exportType.runTaskFnFactory(reporting, logger); jobExecutors.set(exportType.jobType, jobExecutor); diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index d1554a03b9389a..31960c782b7b9c 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -5,15 +5,15 @@ */ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { ReportingCore } from '../'; import { AuthenticatedUser } from '../../../security/server'; -import { ESQueueCreateJobFn } from '../../server/types'; -import { ReportingCore } from '../core'; +import { CreateJobBaseParams, CreateJobFn } from '../types'; import { LevelLogger } from './'; -import { ReportingStore, Report } from './store'; +import { Report } from './store'; export type EnqueueJobFn = ( exportTypeId: string, - jobParams: unknown, + jobParams: CreateJobBaseParams, user: AuthenticatedUser | null, context: RequestHandlerContext, request: KibanaRequest @@ -21,41 +21,39 @@ export type EnqueueJobFn = ( export function enqueueJobFactory( reporting: ReportingCore, - store: ReportingStore, parentLogger: LevelLogger ): EnqueueJobFn { - const config = reporting.getConfig(); - const queueTimeout = config.get('queue', 'timeout'); - const browserType = config.get('capture', 'browser', 'type'); - const maxAttempts = config.get('capture', 'maxAttempts'); const logger = parentLogger.clone(['queue-job']); return async function enqueueJob( exportTypeId: string, - jobParams: unknown, + jobParams: CreateJobBaseParams, user: AuthenticatedUser | null, context: RequestHandlerContext, request: KibanaRequest ) { - type ScheduleTaskFnType = ESQueueCreateJobFn; + type ScheduleTaskFnType = CreateJobFn; - const username = user ? user.username : false; + const username: string | null = user ? user.username : null; const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); if (exportType == null) { throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); } - const scheduleTask = exportType.scheduleTaskFnFactory(reporting, logger) as ScheduleTaskFnType; + const [scheduleTask, { store }] = await Promise.all([ + exportType.scheduleTaskFnFactory(reporting, logger) as ScheduleTaskFnType, + reporting.getPluginStartDeps(), + ]); + + // add encrytped headers const payload = await scheduleTask(jobParams, context, request); - const options = { - timeout: queueTimeout, - created_by: username, - browser_type: browserType, - max_attempts: maxAttempts, - }; + // store the pending report, puts it in the Reporting Management UI table + const report = await store.addReport(exportType.jobType, username, payload); + + logger.info(`Scheduled ${exportType.name} report: ${report._id}`); - return await store.addReport(exportType.jobType, payload, options); + return report; }; } diff --git a/x-pack/plugins/reporting/server/lib/esqueue/constants/index.js b/x-pack/plugins/reporting/server/lib/esqueue/constants/index.js index 5fcff3531851a6..7f7383bb8611d6 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/constants/index.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/constants/index.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { events } from './events'; -import { statuses } from './statuses'; +import { statuses } from '../../statuses'; import { defaultSettings } from './default_settings'; +import { events } from './events'; export const constants = { ...events, diff --git a/x-pack/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/worker.js index 469bafd6946122..0c3a6384f6b9ae 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/worker.js @@ -158,26 +158,18 @@ export class Worker extends events.EventEmitter { kibana_name: this.kibanaName, }; - return this.queue.store - .updateReport({ - index: job._index, - id: job._id, - if_seq_no: job._seq_no, - if_primary_term: job._primary_term, - body: { doc }, - }) - .then((response) => { - this.info(`Job marked as claimed: ${getUpdatedDocPath(response)}`); - const updatedJob = { - ...job, - ...response, - }; - updatedJob._source = { - ...job._source, - ...doc, - }; - return updatedJob; - }); + return this.queue.store.setReportClaimed(job, doc).then((response) => { + this.info(`Job marked as claimed: ${getUpdatedDocPath(response)}`); + const updatedJob = { + ...job, + ...response, + }; + updatedJob._source = { + ...job._source, + ...doc, + }; + return updatedJob; + }); } _failJob(job, output = false) { @@ -198,13 +190,7 @@ export class Worker extends events.EventEmitter { }); return this.queue.store - .updateReport({ - index: job._index, - id: job._id, - if_seq_no: job._seq_no, - if_primary_term: job._primary_term, - body: { doc }, - }) + .setReportFailed(job, doc) .then((response) => { this.info(`Job marked as failed: ${getUpdatedDocPath(response)}`); }) @@ -295,13 +281,7 @@ export class Worker extends events.EventEmitter { }; return this.queue.store - .updateReport({ - index: job._index, - id: job._id, - if_seq_no: job._seq_no, - if_primary_term: job._primary_term, - body: { doc }, - }) + .setReportCompleted(job, doc) .then((response) => { const eventOutput = { job: formatJobObject(job), diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index e4adb1188e3fc4..f3a09cffbb1047 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -8,8 +8,9 @@ export { checkLicense } from './check_license'; export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; -export { getExportTypesRegistry } from './export_types_registry'; +export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; export { LevelLogger } from './level_logger'; +export { statuses } from './statuses'; export { ReportingStore } from './store'; export { startTrace } from './trace'; export { runValidations } from './validate'; diff --git a/x-pack/plugins/reporting/server/lib/esqueue/constants/statuses.ts b/x-pack/plugins/reporting/server/lib/statuses.ts similarity index 100% rename from x-pack/plugins/reporting/server/lib/esqueue/constants/statuses.ts rename to x-pack/plugins/reporting/server/lib/statuses.ts diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts index a819923e2f1054..d08b928cdca4be 100644 --- a/x-pack/plugins/reporting/server/lib/store/mapping.ts +++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts @@ -45,7 +45,7 @@ export const mapping = { priority: { type: 'byte' }, timeout: { type: 'long' }, process_expiration: { type: 'date' }, - created_by: { type: 'keyword' }, + created_by: { type: 'keyword' }, // `null` if security is disabled created_at: { type: 'date' }, started_at: { type: 'date' }, completed_at: { type: 'date' }, diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts index 83444494e61d33..9ac5d1f87c387f 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -8,46 +8,59 @@ import { Report } from './report'; describe('Class Report', () => { it('constructs Report instance', () => { - const opts = { - index: '.reporting-test-index-12345', + const report = new Report({ + _index: '.reporting-test-index-12345', jobtype: 'test-report', created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { payload_test_field: 1 }, + payload: { headers: 'payload_test_field', objectType: 'testOt' }, timeout: 30000, priority: 1, - }; - const report = new Report(opts); - expect(report.toJSON()).toMatchObject({ - _primary_term: undefined, - _seq_no: undefined, + }); + + expect(report.toEsDocsJSON()).toMatchObject({ + _index: '.reporting-test-index-12345', + _source: { + attempts: 0, + browser_type: 'browser_type_test_string', + completed_at: undefined, + created_at: undefined, + created_by: 'created_by_test_string', + jobtype: 'test-report', + max_attempts: 50, + meta: undefined, + payload: { headers: 'payload_test_field', objectType: 'testOt' }, + priority: 1, + started_at: undefined, + status: 'pending', + timeout: 30000, + }, + }); + expect(report.toApiJSON()).toMatchObject({ browser_type: 'browser_type_test_string', created_by: 'created_by_test_string', jobtype: 'test-report', max_attempts: 50, - payload: { - payload_test_field: 1, - }, + payload: { headers: 'payload_test_field', objectType: 'testOt' }, priority: 1, timeout: 30000, }); - expect(report.id).toBeDefined(); + expect(report._id).toBeDefined(); }); - it('updateWithDoc method syncs takes fields to sync ES metadata', () => { - const opts = { - index: '.reporting-test-index-12345', + it('updateWithEsDoc method syncs fields to sync ES metadata', () => { + const report = new Report({ + _index: '.reporting-test-index-12345', jobtype: 'test-report', created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { payload_test_field: 1 }, + payload: { headers: 'payload_test_field', objectType: 'testOt' }, timeout: 30000, priority: 1, - }; - const report = new Report(opts); + }); const metadata = { _index: '.reporting-test-update', @@ -55,23 +68,53 @@ describe('Class Report', () => { _primary_term: 77, _seq_no: 99, }; - report.updateWithDoc(metadata); - - expect(report.toJSON()).toMatchObject({ - index: '.reporting-test-update', - _primary_term: 77, - _seq_no: 99, - browser_type: 'browser_type_test_string', - created_by: 'created_by_test_string', - jobtype: 'test-report', - max_attempts: 50, - payload: { - payload_test_field: 1, - }, - priority: 1, - timeout: 30000, - }); + report.updateWithEsDoc(metadata); - expect(report._id).toBe('12342p9o387549o2345'); + expect(report.toEsDocsJSON()).toMatchInlineSnapshot(` + Object { + "_id": "12342p9o387549o2345", + "_index": ".reporting-test-update", + "_source": Object { + "attempts": 0, + "browser_type": "browser_type_test_string", + "completed_at": undefined, + "created_at": undefined, + "created_by": "created_by_test_string", + "jobtype": "test-report", + "max_attempts": 50, + "meta": undefined, + "payload": Object { + "headers": "payload_test_field", + "objectType": "testOt", + }, + "priority": 1, + "started_at": undefined, + "status": "pending", + "timeout": 30000, + }, + } + `); + expect(report.toApiJSON()).toMatchInlineSnapshot(` + Object { + "attempts": 0, + "browser_type": "browser_type_test_string", + "completed_at": undefined, + "created_at": undefined, + "created_by": "created_by_test_string", + "id": "12342p9o387549o2345", + "index": ".reporting-test-update", + "jobtype": "test-report", + "max_attempts": 50, + "meta": undefined, + "payload": Object { + "headers": "payload_test_field", + "objectType": "testOt", + }, + "priority": 1, + "started_at": undefined, + "status": "pending", + "timeout": 30000, + } + `); }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index cc9967e64b6ebc..5ff71ae7a71824 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -6,80 +6,158 @@ // @ts-ignore no module definition import Puid from 'puid'; +import { JobStatuses } from '../../../constants'; +import { LayoutInstance } from '../layouts'; -interface Payload { - id?: string; - index: string; +/* + * The document created by Reporting to store in the .reporting index + */ +interface ReportingDocument { + _id: string; + _index: string; + _seq_no: unknown; + _primary_term: unknown; jobtype: string; - created_by: string | boolean; - payload: unknown; + created_by: string | null; + payload: { + headers: string; // encrypted headers + objectType: string; + layout?: LayoutInstance; + }; + meta: unknown; browser_type: string; - priority: number; max_attempts: number; timeout: number; + + status: string; + attempts: number; + output?: unknown; + started_at?: string; + completed_at?: string; + created_at?: string; + priority?: number; + process_expiration?: string; } +/* + * The document created by Reporting to store as task parameters for Task + * Manager to reference the report in .reporting + */ const puid = new Puid(); -export class Report { - public readonly jobtype: string; - public readonly created_by: string | boolean; - public readonly payload: unknown; - public readonly browser_type: string; - public readonly id: string; +export class Report implements Partial { + public _index?: string; + public _id: string; + public _primary_term?: unknown; // set by ES + public _seq_no: unknown; // set by ES - public readonly priority: number; - // queue stuff, to be removed with Task Manager integration + public readonly jobtype: string; + public readonly created_at?: string; + public readonly created_by?: string | null; + public readonly payload: { + headers: string; // encrypted headers + objectType: string; + layout?: LayoutInstance; + }; + public readonly meta: unknown; public readonly max_attempts: number; - public readonly timeout: number; + public readonly browser_type?: string; - public _index: string; - public _id?: string; // set by ES - public _primary_term?: unknown; // set by ES - public _seq_no: unknown; // set by ES + public readonly status: string; + public readonly attempts: number; + public readonly output?: unknown; + public readonly started_at?: string; + public readonly completed_at?: string; + public readonly process_expiration?: string; + public readonly priority?: number; + public readonly timeout?: number; /* * Create an unsaved report */ - constructor(opts: Payload) { - this.jobtype = opts.jobtype; + constructor(opts: Partial) { + this._id = opts._id != null ? opts._id : puid.generate(); + this._index = opts._index; + this._primary_term = opts._primary_term; + this._seq_no = opts._seq_no; + + this.payload = opts.payload!; + this.jobtype = opts.jobtype!; + this.max_attempts = opts.max_attempts!; + this.attempts = opts.attempts || 0; + + this.process_expiration = opts.process_expiration; + this.timeout = opts.timeout; + + this.created_at = opts.created_at; this.created_by = opts.created_by; - this.payload = opts.payload; + this.meta = opts.meta; this.browser_type = opts.browser_type; this.priority = opts.priority; - this.max_attempts = opts.max_attempts; - this.timeout = opts.timeout; - this.id = puid.generate(); - this._index = opts.index; + this.status = opts.status || JobStatuses.PENDING; + this.output = opts.output || null; } /* * Update the report with "live" storage metadata */ - updateWithDoc(doc: Partial) { - if (doc._index) { - this._index = doc._index; // can not be undefined + updateWithEsDoc(doc: Partial) { + if (doc._index == null || doc._id == null) { + throw new Error(`Report object from ES has missing fields!`); } this._id = doc._id; + this._index = doc._index; this._primary_term = doc._primary_term; this._seq_no = doc._seq_no; } - toJSON() { + /* + * Data structure for writing to Elasticsearch index + */ + toEsDocsJSON() { + return { + _id: this._id, + _index: this._index, + _source: { + jobtype: this.jobtype, + created_at: this.created_at, + created_by: this.created_by, + payload: this.payload, + meta: this.meta, + timeout: this.timeout, + max_attempts: this.max_attempts, + priority: this.priority, + browser_type: this.browser_type, + status: this.status, + attempts: this.attempts, + started_at: this.started_at, + completed_at: this.completed_at, + }, + }; + } + + /* + * Data structure for API responses + */ + toApiJSON() { return { - id: this.id, + id: this._id, index: this._index, - _seq_no: this._seq_no, - _primary_term: this._primary_term, jobtype: this.jobtype, + created_at: this.created_at, created_by: this.created_by, payload: this.payload, + meta: this.meta, timeout: this.timeout, max_attempts: this.max_attempts, priority: this.priority, browser_type: this.browser_type, + status: this.status, + attempts: this.attempts, + started_at: this.started_at, + completed_at: this.completed_at, }; } } diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 4868a1dfdd8f3a..c66e2dd7742c4f 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -5,11 +5,12 @@ */ import sinon from 'sinon'; +import { ElasticsearchServiceSetup } from 'src/core/server'; import { ReportingConfig, ReportingCore } from '../..'; import { createMockReportingCore } from '../../test_helpers'; import { createMockLevelLogger } from '../../test_helpers/create_mock_levellogger'; +import { Report } from './report'; import { ReportingStore } from './store'; -import { ElasticsearchServiceSetup } from 'src/core/server'; const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ get: mockConfigGet, @@ -31,11 +32,13 @@ describe('ReportingStore', () => { mockConfig = getMockConfig(mockConfigGet); mockCore = await createMockReportingCore(mockConfig); + callClusterStub.reset(); callClusterStub.withArgs('indices.exists').resolves({}); callClusterStub.withArgs('indices.create').resolves({}); - callClusterStub.withArgs('index').resolves({}); + callClusterStub.withArgs('index').resolves({ _id: 'stub-id', _index: 'stub-index' }); callClusterStub.withArgs('indices.refresh').resolves({}); callClusterStub.withArgs('update').resolves({}); + callClusterStub.withArgs('get').resolves({}); mockCore.getElasticsearchService = () => (mockElasticsearch as unknown) as ElasticsearchServiceSetup; @@ -45,25 +48,25 @@ describe('ReportingStore', () => { it('returns Report object', async () => { const store = new ReportingStore(mockCore, mockLogger); const reportType = 'unknowntype'; - const reportPayload = {}; - const reportOptions = { - timeout: 10000, - created_by: 'created_by_string', - browser_type: 'browser_type_string', - max_attempts: 1, + const reportPayload = { + browserTimezone: 'UTC', + headers: 'rp_headers_1', + objectType: 'testOt', }; - await expect( - store.addReport(reportType, reportPayload, reportOptions) - ).resolves.toMatchObject({ + await expect(store.addReport(reportType, 'username1', reportPayload)).resolves.toMatchObject({ _primary_term: undefined, _seq_no: undefined, - browser_type: 'browser_type_string', - created_by: 'created_by_string', + attempts: 0, + browser_type: undefined, + completed_at: undefined, + created_by: 'username1', jobtype: 'unknowntype', - max_attempts: 1, + max_attempts: undefined, payload: {}, priority: 10, - timeout: 10000, + started_at: undefined, + status: 'pending', + timeout: undefined, }); }); @@ -76,35 +79,31 @@ describe('ReportingStore', () => { const store = new ReportingStore(mockCore, mockLogger); const reportType = 'unknowntype'; - const reportPayload = {}; - const reportOptions = { - timeout: 10000, - created_by: 'created_by_string', - browser_type: 'browser_type_string', - max_attempts: 1, + const reportPayload = { + browserTimezone: 'UTC', + headers: 'rp_headers_2', + objectType: 'testOt', }; - expect( - store.addReport(reportType, reportPayload, reportOptions) - ).rejects.toMatchInlineSnapshot(`[Error: Invalid index interval: centurially]`); + expect(store.addReport(reportType, 'user1', reportPayload)).rejects.toMatchInlineSnapshot( + `[Error: Invalid index interval: centurially]` + ); }); it('handles error creating the index', async () => { // setup callClusterStub.withArgs('indices.exists').resolves(false); - callClusterStub.withArgs('indices.create').rejects(new Error('error')); + callClusterStub.withArgs('indices.create').rejects(new Error('horrible error')); const store = new ReportingStore(mockCore, mockLogger); const reportType = 'unknowntype'; - const reportPayload = {}; - const reportOptions = { - timeout: 10000, - created_by: 'created_by_string', - browser_type: 'browser_type_string', - max_attempts: 1, + const reportPayload = { + browserTimezone: 'UTC', + headers: 'rp_headers_3', + objectType: 'testOt', }; await expect( - store.addReport(reportType, reportPayload, reportOptions) - ).rejects.toMatchInlineSnapshot(`[Error: error]`); + store.addReport(reportType, 'user1', reportPayload) + ).rejects.toMatchInlineSnapshot(`[Error: horrible error]`); }); /* Creating the index will fail, if there were multiple jobs staged in @@ -116,20 +115,18 @@ describe('ReportingStore', () => { it('ignores index creation error if the index already exists and continues adding the report', async () => { // setup callClusterStub.withArgs('indices.exists').resolves(false); - callClusterStub.withArgs('indices.create').rejects(new Error('error')); + callClusterStub.withArgs('indices.create').rejects(new Error('devastating error')); const store = new ReportingStore(mockCore, mockLogger); const reportType = 'unknowntype'; - const reportPayload = {}; - const reportOptions = { - timeout: 10000, - created_by: 'created_by_string', - browser_type: 'browser_type_string', - max_attempts: 1, + const reportPayload = { + browserTimezone: 'UTC', + headers: 'rp_headers_4', + objectType: 'testOt', }; await expect( - store.addReport(reportType, reportPayload, reportOptions) - ).rejects.toMatchInlineSnapshot(`[Error: error]`); + store.addReport(reportType, 'user1', reportPayload) + ).rejects.toMatchInlineSnapshot(`[Error: devastating error]`); }); it('skips creating the index if already exists', async () => { @@ -141,26 +138,223 @@ describe('ReportingStore', () => { const store = new ReportingStore(mockCore, mockLogger); const reportType = 'unknowntype'; - const reportPayload = {}; - const reportOptions = { - timeout: 10000, - created_by: 'created_by_string', - browser_type: 'browser_type_string', - max_attempts: 1, + const reportPayload = { + browserTimezone: 'UTC', + headers: 'rp_headers_5', + objectType: 'testOt', }; - await expect( - store.addReport(reportType, reportPayload, reportOptions) - ).resolves.toMatchObject({ + await expect(store.addReport(reportType, 'user1', reportPayload)).resolves.toMatchObject({ + _primary_term: undefined, + _seq_no: undefined, + attempts: 0, + browser_type: undefined, + completed_at: undefined, + created_by: 'user1', + jobtype: 'unknowntype', + max_attempts: undefined, + payload: {}, + priority: 10, + started_at: undefined, + status: 'pending', + timeout: undefined, + }); + }); + + it('allows username string to be `null`', async () => { + // setup + callClusterStub.withArgs('indices.exists').resolves(false); + callClusterStub + .withArgs('indices.create') + .rejects(new Error('resource_already_exists_exception')); // will be triggered but ignored + + const store = new ReportingStore(mockCore, mockLogger); + const reportType = 'unknowntype'; + const reportPayload = { + browserTimezone: 'UTC', + headers: 'rp_test_headers', + objectType: 'testOt', + }; + await expect(store.addReport(reportType, null, reportPayload)).resolves.toMatchObject({ _primary_term: undefined, _seq_no: undefined, - browser_type: 'browser_type_string', - created_by: 'created_by_string', + attempts: 0, + browser_type: undefined, + completed_at: undefined, + created_by: null, jobtype: 'unknowntype', - max_attempts: 1, + max_attempts: undefined, payload: {}, priority: 10, - timeout: 10000, + started_at: undefined, + status: 'pending', + timeout: undefined, }); }); }); + + it('setReportClaimed sets the status of a record to processing', async () => { + const store = new ReportingStore(mockCore, mockLogger); + const report = new Report({ + _id: 'id-of-processing', + _index: '.reporting-test-index-12345', + jobtype: 'test-report', + created_by: 'created_by_test_string', + browser_type: 'browser_type_test_string', + max_attempts: 50, + payload: { + headers: 'rp_test_headers', + objectType: 'testOt', + }, + timeout: 30000, + priority: 1, + }); + + await store.setReportClaimed(report, { testDoc: 'test' } as any); + + const updateCall = callClusterStub.getCalls().find((call) => call.args[0] === 'update'); + expect(updateCall && updateCall.args).toMatchInlineSnapshot(` + Array [ + "update", + Object { + "body": Object { + "doc": Object { + "status": "processing", + "testDoc": "test", + }, + }, + "id": "id-of-processing", + "if_primary_term": undefined, + "if_seq_no": undefined, + "index": ".reporting-test-index-12345", + }, + ] + `); + }); + + it('setReportFailed sets the status of a record to failed', async () => { + const store = new ReportingStore(mockCore, mockLogger); + const report = new Report({ + _id: 'id-of-failure', + _index: '.reporting-test-index-12345', + jobtype: 'test-report', + created_by: 'created_by_test_string', + browser_type: 'browser_type_test_string', + max_attempts: 50, + payload: { + headers: 'rp_test_headers', + objectType: 'testOt', + }, + timeout: 30000, + priority: 1, + }); + + await store.setReportFailed(report, { errors: 'yes' } as any); + + const updateCall = callClusterStub.getCalls().find((call) => call.args[0] === 'update'); + expect(updateCall && updateCall.args).toMatchInlineSnapshot(` + Array [ + "update", + Object { + "body": Object { + "doc": Object { + "errors": "yes", + "status": "failed", + }, + }, + "id": "id-of-failure", + "if_primary_term": undefined, + "if_seq_no": undefined, + "index": ".reporting-test-index-12345", + }, + ] + `); + }); + + it('setReportCompleted sets the status of a record to completed', async () => { + const store = new ReportingStore(mockCore, mockLogger); + const report = new Report({ + _id: 'vastly-great-report-id', + _index: '.reporting-test-index-12345', + jobtype: 'test-report', + created_by: 'created_by_test_string', + browser_type: 'browser_type_test_string', + max_attempts: 50, + payload: { + headers: 'rp_test_headers', + objectType: 'testOt', + }, + timeout: 30000, + priority: 1, + }); + + await store.setReportCompleted(report, { certainly_completed: 'yes' } as any); + + const updateCall = callClusterStub.getCalls().find((call) => call.args[0] === 'update'); + expect(updateCall && updateCall.args).toMatchInlineSnapshot(` + Array [ + "update", + Object { + "body": Object { + "doc": Object { + "certainly_completed": "yes", + "status": "completed", + }, + }, + "id": "vastly-great-report-id", + "if_primary_term": undefined, + "if_seq_no": undefined, + "index": ".reporting-test-index-12345", + }, + ] + `); + }); + + it('setReportCompleted sets the status of a record to completed_with_warnings', async () => { + const store = new ReportingStore(mockCore, mockLogger); + const report = new Report({ + _id: 'vastly-great-report-id', + _index: '.reporting-test-index-12345', + jobtype: 'test-report', + created_by: 'created_by_test_string', + browser_type: 'browser_type_test_string', + max_attempts: 50, + payload: { + headers: 'rp_test_headers', + objectType: 'testOt', + }, + timeout: 30000, + priority: 1, + }); + + await store.setReportCompleted(report, { + certainly_completed: 'pretty_much', + output: { + warnings: [`those pants don't go with that shirt`], + }, + } as any); + + const updateCall = callClusterStub.getCalls().find((call) => call.args[0] === 'update'); + expect(updateCall && updateCall.args).toMatchInlineSnapshot(` + Array [ + "update", + Object { + "body": Object { + "doc": Object { + "certainly_completed": "pretty_much", + "output": Object { + "warnings": Array [ + "those pants don't go with that shirt", + ], + }, + "status": "completed_with_warnings", + }, + }, + "id": "vastly-great-report-id", + "if_primary_term": undefined, + "if_seq_no": undefined, + "index": ".reporting-test-index-12345", + }, + ] + `); + }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 0f1ed83b717671..12cff0e973ed62 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -5,36 +5,24 @@ */ import { ElasticsearchServiceSetup } from 'src/core/server'; -import { LevelLogger } from '../'; +import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; +import { CreateJobBaseParams, CreateJobBaseParamsEncryptedFields } from '../../types'; import { indexTimestamp } from './index_timestamp'; -import { LayoutInstance } from '../layouts'; import { mapping } from './mapping'; import { Report } from './report'; - -export const statuses = { - JOB_STATUS_PENDING: 'pending', - JOB_STATUS_PROCESSING: 'processing', - JOB_STATUS_COMPLETED: 'completed', - JOB_STATUS_WARNINGS: 'completed_with_warnings', - JOB_STATUS_FAILED: 'failed', - JOB_STATUS_CANCELLED: 'cancelled', -}; - -interface AddReportOpts { +interface JobSettings { timeout: number; - created_by: string | boolean; browser_type: string; max_attempts: number; + priority: number; } -interface UpdateQuery { - index: string; - id: string; - if_seq_no: unknown; - if_primary_term: unknown; - body: { doc: Partial }; -} +const checkReportIsEditable = (report: Report) => { + if (!report._id || !report._index) { + throw new Error(`Report object is not synced with ES!`); + } +}; /* * A class to give an interface to historical reports in the reporting.index @@ -43,9 +31,9 @@ interface UpdateQuery { * - interface for downloading the report */ export class ReportingStore { - public readonly indexPrefix: string; - public readonly indexInterval: string; - + private readonly indexPrefix: string; + private readonly indexInterval: string; + private readonly jobSettings: JobSettings; private client: ElasticsearchServiceSetup['legacy']['client']; private logger: LevelLogger; @@ -56,12 +44,18 @@ export class ReportingStore { this.client = elasticsearch.legacy.client; this.indexPrefix = config.get('index'); this.indexInterval = config.get('queue', 'indexInterval'); + this.jobSettings = { + timeout: config.get('queue', 'timeout'), + browser_type: config.get('capture', 'browser', 'type'), + max_attempts: config.get('capture', 'maxAttempts'), + priority: 10, // unused + }; this.logger = logger; } private async createIndex(indexName: string) { - return this.client + return await this.client .callAsInternalUser('indices.exists', { index: indexName, }) @@ -95,75 +89,157 @@ export class ReportingStore { return; } + this.logger.error(err); throw err; }); }); } - private async saveReport(report: Report) { - const payload = report.payload as { objectType: string; layout: LayoutInstance }; + /* + * Called from addReport, which handles any errors + */ + private async indexReport(report: Report) { + const params = report.payload; + + // Queing is handled by TM. These queueing-based fields for reference in Report Info panel + const infoFields = { + timeout: report.timeout, + process_expiration: new Date(0), // use epoch so the job query works + created_at: new Date(), + attempts: 0, + max_attempts: report.max_attempts, + status: statuses.JOB_STATUS_PENDING, + browser_type: report.browser_type, + }; const indexParams = { index: report._index, - id: report.id, + id: report._id, body: { + ...infoFields, jobtype: report.jobtype, meta: { // We are copying these values out of payload because these fields are indexed and can be aggregated on // for tracking stats, while payload contents are not. - objectType: payload.objectType, - layout: payload.layout ? payload.layout.id : 'none', + objectType: params.objectType, + layout: params.layout ? params.layout.id : 'none', }, payload: report.payload, created_by: report.created_by, - timeout: report.timeout, - process_expiration: new Date(0), // use epoch so the job query works - created_at: new Date(), - attempts: 0, - max_attempts: report.max_attempts, - status: statuses.JOB_STATUS_PENDING, - browser_type: report.browser_type, }, }; - return this.client.callAsInternalUser('index', indexParams); + return await this.client.callAsInternalUser('index', indexParams); } + /* + * Called from addReport, which handles any errors + */ private async refreshIndex(index: string) { - return this.client.callAsInternalUser('indices.refresh', { index }); + return await this.client.callAsInternalUser('indices.refresh', { index }); } - public async addReport(type: string, payload: unknown, options: AddReportOpts): Promise { + public async addReport( + type: string, + username: string | null, + payload: CreateJobBaseParams & CreateJobBaseParamsEncryptedFields + ): Promise { const timestamp = indexTimestamp(this.indexInterval); const index = `${this.indexPrefix}-${timestamp}`; await this.createIndex(index); const report = new Report({ - index, + _index: index, payload, jobtype: type, - created_by: options.created_by, - browser_type: options.browser_type, - max_attempts: options.max_attempts, - timeout: options.timeout, - priority: 10, // unused + created_by: username, + ...this.jobSettings, }); - const doc = await this.saveReport(report); - report.updateWithDoc(doc); + try { + const doc = await this.indexReport(report); + report.updateWithEsDoc(doc); - await this.refreshIndex(index); - this.logger.info(`Successfully queued pending job: ${report._index}/${report.id}`); + await this.refreshIndex(index); + this.logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`); - return report; + return report; + } catch (err) { + this.logger.error(`Error in addReport!`); + this.logger.error(err); + throw err; + } } - public async updateReport(query: UpdateQuery): Promise { - return this.client.callAsInternalUser('update', { - index: query.index, - id: query.id, - if_seq_no: query.if_seq_no, - if_primary_term: query.if_primary_term, - body: { doc: query.body.doc }, - }); + public async setReportClaimed(report: Report, stats: Partial): Promise { + const doc = { + ...stats, + status: statuses.JOB_STATUS_PROCESSING, + }; + + try { + checkReportIsEditable(report); + + return await this.client.callAsInternalUser('update', { + id: report._id, + index: report._index, + if_seq_no: report._seq_no, + if_primary_term: report._primary_term, + body: { doc }, + }); + } catch (err) { + this.logger.error('Error in setting report processing status!'); + this.logger.error(err); + throw err; + } + } + + public async setReportFailed(report: Report, stats: Partial): Promise { + const doc = { + ...stats, + status: statuses.JOB_STATUS_FAILED, + }; + + try { + checkReportIsEditable(report); + + return await this.client.callAsInternalUser('update', { + id: report._id, + index: report._index, + if_seq_no: report._seq_no, + if_primary_term: report._primary_term, + body: { doc }, + }); + } catch (err) { + this.logger.error('Error in setting report failed status!'); + this.logger.error(err); + throw err; + } + } + + public async setReportCompleted(report: Report, stats: Partial): Promise { + try { + const { output } = stats as { output: any }; + const status = + output && output.warnings && output.warnings.length > 0 + ? statuses.JOB_STATUS_WARNINGS + : statuses.JOB_STATUS_COMPLETED; + const doc = { + ...stats, + status, + }; + checkReportIsEditable(report); + + return await this.client.callAsInternalUser('update', { + id: report._id, + index: report._index, + if_seq_no: report._seq_no, + if_primary_term: report._primary_term, + body: { doc }, + }); + } catch (err) { + this.logger.error('Error in setting report complete status!'); + this.logger.error(err); + throw err; + } } } diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index cedc9dc14a2376..20e22c2db00e35 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -8,13 +8,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core import { ReportingCore } from './'; import { initializeBrowserDriverFactory } from './browsers'; import { buildConfig, ReportingConfigType } from './config'; -import { - createQueueFactory, - enqueueJobFactory, - LevelLogger, - runValidations, - ReportingStore, -} from './lib'; +import { createQueueFactory, LevelLogger, runValidations, ReportingStore } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; @@ -94,14 +88,12 @@ export class ReportingPlugin const browserDriverFactory = await initializeBrowserDriverFactory(config, logger); const store = new ReportingStore(reportingCore, logger); const esqueue = await createQueueFactory(reportingCore, store, logger); // starts polling for pending jobs - const enqueueJob = enqueueJobFactory(reportingCore, store, logger); // called from generation routes reportingCore.pluginStart({ browserDriverFactory, savedObjects: core.savedObjects, uiSettings: core.uiSettings, esqueue, - enqueueJob, store, }); diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index 2a12a64d67a354..f4959b56dfea1e 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -10,6 +10,7 @@ import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routi import { HandlerErrorFunction, HandlerFunction } from './types'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; +import { CreateJobBaseParams } from '../types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; @@ -44,13 +45,13 @@ export function registerGenerateFromJobParams( }, }, userHandler(async (user, context, req, res) => { - let jobParamsRison: string | null; + let jobParamsRison: null | string = null; if (req.body) { - const { jobParams: jobParamsPayload } = req.body as { jobParams: string }; - jobParamsRison = jobParamsPayload; - } else { - const { jobParams: queryJobParams } = req.query as { jobParams: string }; + const { jobParams: jobParamsPayload } = req.body; + jobParamsRison = jobParamsPayload ? jobParamsPayload : null; + } else if (req.query?.jobParams) { + const { jobParams: queryJobParams } = req.query; if (queryJobParams) { jobParamsRison = queryJobParams; } else { @@ -65,11 +66,11 @@ export function registerGenerateFromJobParams( }); } - const { exportType } = req.params as { exportType: string }; + const { exportType } = req.params; let jobParams; try { - jobParams = rison.decode(jobParamsRison) as object | null; + jobParams = rison.decode(jobParamsRison) as CreateJobBaseParams | null; if (!jobParams) { return res.customError({ statusCode: 400, diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 8250ca462049b7..a0a8f25de7fc49 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -5,16 +5,24 @@ */ import { schema } from '@kbn/config-schema'; +import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; +import { JobParamsPostPayloadPanelCsv } from '../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; import { TaskRunResult } from '../types'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; import { getJobParamsFromRequest } from './lib/get_job_params_from_request'; import { HandlerErrorFunction } from './types'; +export type CsvFromSavedObjectRequest = KibanaRequest< + { savedObjectType: string; savedObjectId: string }, + unknown, + JobParamsPostPayloadPanelCsv +>; + /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: * - saved object type and ID @@ -56,7 +64,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( }), }, }, - userHandler(async (user, context, req, res) => { + userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); const scheduleTaskFn = scheduleTaskFnFactory(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index 87a696948ad84d..cef4da9aabbd47 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -138,8 +138,7 @@ describe('POST /api/reporting/generate', () => { }); it('returns 500 if job handler throws an error', async () => { - // throw an error from enqueueJob - core.getEnqueueJob = jest.fn().mockRejectedValue('Sorry, this tests says no'); + callClusterStub.withArgs('index').rejects('silly'); registerJobGenerationRoutes(core, mockLogger); diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts index 017e875931ae2c..b2115076aada4c 100644 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ b/x-pack/plugins/reporting/server/routes/generation.ts @@ -10,6 +10,7 @@ import { kibanaResponseFactory } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; +import { enqueueJobFactory } from '../lib/enqueue_job'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; import { HandlerFunction } from './types'; @@ -43,11 +44,10 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo } try { - const enqueueJob = await reporting.getEnqueueJob(); - const job = await enqueueJob(exportTypeId, jobParams, user, context, req); + const enqueueJob = enqueueJobFactory(reporting, logger); + const report = await enqueueJob(exportTypeId, jobParams, user, context, req); // return the queue's job information - const jobJson = job.toJSON(); const downloadBaseUrl = getDownloadBaseUrl(reporting); return res.ok({ @@ -55,8 +55,8 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo 'content-type': 'application/json', }, body: { - path: `${downloadBaseUrl}/${jobJson.id}`, - job: jobJson, + path: `${downloadBaseUrl}/${report._id}`, + job: report.toApiJSON(), }, }); } catch (err) { diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index d384cbb878a0ed..84a98d6d1f1d7a 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -8,8 +8,7 @@ import contentDisposition from 'content-disposition'; import { get } from 'lodash'; import { CSV_JOB_TYPE } from '../../../common/constants'; -import { statuses } from '../../lib/esqueue/constants/statuses'; -import { ExportTypesRegistry } from '../../lib/export_types_registry'; +import { ExportTypesRegistry, statuses } from '../../lib'; import { ExportTypeDefinition, JobSource, TaskRunResult } from '../../types'; type ExportTypeType = ExportTypeDefinition; @@ -18,11 +17,11 @@ interface ErrorFromPayload { message: string; } -// A camelCase version of TaskRunResult +// interface of the API result interface Payload { statusCode: number; content: string | Buffer | ErrorFromPayload; - contentType: string; + contentType: string | null; headers: Record; } diff --git a/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts b/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts index e5c1f382413497..bfa15a4022a4d6 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts @@ -4,21 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'src/core/server'; -import { - JobParamsPanelCsv, - JobParamsPostPayloadPanelCsv, -} from '../../export_types/csv_from_savedobject/types'; +import { JobParamsPanelCsv } from '../../export_types/csv_from_savedobject/types'; +import { CsvFromSavedObjectRequest } from '../generate_from_savedobject_immediate'; export function getJobParamsFromRequest( - request: KibanaRequest, + request: CsvFromSavedObjectRequest, opts: { isImmediate: boolean } ): JobParamsPanelCsv { - const { savedObjectType, savedObjectId } = request.params as { - savedObjectType: string; - savedObjectId: string; - }; - const { timerange, state } = request.body as JobParamsPostPayloadPanelCsv; + const { savedObjectType, savedObjectId } = request.params; + const { timerange, state } = request.body; const post = timerange || state ? { timerange, state } : undefined; diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts index 607ce34ab94652..c92c9fb7eef749 100644 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/plugins/reporting/server/routes/types.d.ts @@ -6,12 +6,12 @@ import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; -import { ScheduledTaskParams } from '../types'; +import { CreateJobBaseParams, ScheduledTaskParams } from '../types'; export type HandlerFunction = ( user: AuthenticatedUser | null, exportType: string, - jobParams: object, + jobParams: CreateJobBaseParams, context: RequestHandlerContext, req: KibanaRequest, res: KibanaResponseFactory diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 95b06aa39f07e4..c508ee6974ca00 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -8,7 +8,6 @@ jest.mock('../routes'); jest.mock('../usage'); jest.mock('../browsers'); jest.mock('../lib/create_queue'); -jest.mock('../lib/enqueue_job'); jest.mock('../lib/validate'); import * as Rx from 'rxjs'; @@ -19,10 +18,9 @@ import { initializeBrowserDriverFactory, } from '../browsers'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; -import { ReportingStartDeps } from '../types'; import { ReportingStore } from '../lib'; +import { ReportingStartDeps } from '../types'; import { createMockLevelLogger } from './create_mock_levellogger'; -import { Report } from '../lib/store'; (initializeBrowserDriverFactory as jest.Mock< Promise @@ -30,10 +28,13 @@ import { Report } from '../lib/store'; (chromium as any).createDriverFactory.mockImplementation(() => ({})); -const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup => { +const createMockPluginSetup = ( + mockReportingCore: ReportingCore, + setupMock?: any +): ReportingInternalSetup => { return { elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } }, - basePath: setupMock.basePath, + basePath: setupMock.basePath || '/all-about-that-basepath', router: setupMock.router, security: setupMock.security, licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) } as any, @@ -48,7 +49,6 @@ const createMockPluginStart = ( const store = new ReportingStore(mockReportingCore, logger); return { browserDriverFactory: startMock.browserDriverFactory, - enqueueJob: startMock.enqueueJob || jest.fn().mockResolvedValue(new Report({} as any)), esqueue: startMock.esqueue, savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() }, uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, @@ -72,15 +72,14 @@ export const createMockReportingCore = async ( setupDepsMock: ReportingInternalSetup | undefined = undefined, startDepsMock: ReportingInternalStart | undefined = undefined ) => { - if (!setupDepsMock) { - setupDepsMock = createMockPluginSetup({}); - } - const mockReportingCore = { getConfig: () => config, getElasticsearchService: () => setupDepsMock?.elasticsearch, } as ReportingCore; + if (!setupDepsMock) { + setupDepsMock = createMockPluginSetup(mockReportingCore, {}); + } if (!startDepsMock) { startDepsMock = createMockPluginStart(mockReportingCore, {}); } diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index ff597b53ea0b0b..c9649cb6e558b2 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -19,24 +19,12 @@ import { LevelLogger } from './lib'; import { LayoutInstance } from './lib/layouts'; /* - * Routing / API types + * Routing types */ -interface ListQuery { - page: string; - size: string; - ids?: string; // optional field forbids us from extending RequestQuery -} - -interface GenerateQuery { - jobParams: string; -} - -export type ReportingRequestQuery = ListQuery | GenerateQuery; - export interface ReportingRequestPre { management: { - jobTypes: any; + jobTypes: string[]; }; user: string; } @@ -54,12 +42,14 @@ export interface TimeRangeParams { max?: Date | string | number | null; } +// the "raw" data coming from the client, unencrypted export interface JobParamPostPayload { timerange?: TimeRangeParams; } +// the pre-processed, encrypted data ready for storage export interface ScheduledTaskParams { - headers?: string; // serialized encrypted headers + headers: string; // serialized encrypted headers jobParams: JobParamsType; title: string; type: string; @@ -77,10 +67,10 @@ export interface JobSource { } export interface TaskRunResult { - content_type: string; + content_type: string | null; content: string | null; - size: number; csv_contains_formulas?: boolean; + size: number; max_size_reached?: boolean; warnings?: string[]; } @@ -177,17 +167,29 @@ export type ReportingSetup = object; export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; -export type ESQueueCreateJobFn = ( +export interface CreateJobBaseParams { + browserTimezone: string; + layout?: LayoutInstance; // for screenshot type reports + objectType: string; +} + +export interface CreateJobBaseParamsEncryptedFields extends CreateJobBaseParams { + basePath?: string; // for screenshot type reports + headers: string; // encrypted headers +} + +export type CreateJobFn = ( jobParams: JobParamsType, context: RequestHandlerContext, request: KibanaRequest -) => Promise; +) => Promise; -export type ESQueueWorkerExecuteFn = ( +// rename me +export type WorkerExecuteFn = ( jobId: string, job: ScheduledTaskParamsType, cancellationToken: CancellationToken -) => Promise; +) => Promise; export type ScheduleTaskFnFactory = ( reporting: ReportingCore, From 875f7701c37b0737208fd51e70a9c60f74fea8a3 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 13 Aug 2020 16:45:16 -0500 Subject: [PATCH 08/46] Add public url to Workplace Search plugin (#74991) --- x-pack/plugins/enterprise_search/public/plugin.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 74263fb36a9581..42ad7de93b00e3 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -73,6 +73,8 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome } = coreStart; chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); + await this.setPublicUrl(config, coreStart.http); + const { renderApp } = await import('./applications'); const { WorkplaceSearch } = await import('./applications/workplace_search'); From 1729091ddfa466ddf447c55ef90ddd8c0a87d9a4 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 13 Aug 2020 17:50:10 -0400 Subject: [PATCH 09/46] [Resolver] Stale query string values are removed when resolver's component instance ID changes. (#74979) The app can show more than 1 Resolver at a time. Each instance has a unique ID called the `resolverComponentInstanceID`. When the user interacts with Resolver it will add values to the query string. The query string values will contain the `resolverComponentInstanceID`. This allows each Resolver to keep its state separate. When resolver unmounts it will remove any query string values related to it. If Resolver's `resolverComponentInstanceID` changes it should remove query string values related to the old instance ID. It does not. This PR fixes that. Note: I don't know if it was possible for this bug to actually happen. I can't make it happen, but depending on how Resolver is mounted by its consumers it *could* --- .../test_utilities/simulator/index.tsx | 38 +++++--- .../resolver/view/clickthrough.test.tsx | 10 +-- .../public/resolver/view/panel.test.tsx | 8 +- .../public/resolver/view/query_params.test.ts | 89 +++++++++++++++++++ .../view/resolver_without_providers.tsx | 9 +- .../resolver/view/use_query_string_keys.ts | 21 +++++ .../view/use_resolver_query_params.ts | 64 ++++--------- .../view/use_resolver_query_params_cleaner.ts | 53 +++++++++++ 8 files changed, 220 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/use_query_string_keys.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params_cleaner.ts diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 355b53e3740925..14cdc26c80f53a 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -22,10 +22,6 @@ import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_fac * Test a Resolver instance using jest, enzyme, and a mock data layer. */ export class Simulator { - /** - * A string that uniquely identifies this Resolver instance among others mounted in the DOM. - */ - private readonly resolverComponentInstanceID: string; /** * The redux store, creating in the constructor using the `dataAccessLayer`. * This code subscribes to state transitions. @@ -69,7 +65,6 @@ export class Simulator { databaseDocumentID?: string; history?: HistoryPackageHistoryInterface; }) { - this.resolverComponentInstanceID = resolverComponentInstanceID; // create the spy middleware (for debugging tests) this.spyMiddleware = spyMiddlewareFactory(); @@ -98,7 +93,7 @@ export class Simulator { // Render Resolver via the `MockResolver` component, using `enzyme`. this.wrapper = mount( ({ // the query string has a key showing that the second child is selected - queryStringSelectedNode: simulator.queryStringValues().selectedNode, + search: simulator.historyLocationSearch, // the second child is rendered in the DOM, and shows up as selected selectedSecondChildNodeCount: simulator.selectedProcessNode(entityIDs.secondChild) .length, @@ -102,7 +103,9 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', })) ).toYieldEqualTo({ // Just the second child should be marked as selected in the query string - queryStringSelectedNode: [entityIDs.secondChild], + search: urlSearch(resolverComponentInstanceID, { + selectedEntityID: entityIDs.secondChild, + }), // The second child is rendered and has `[aria-selected]` selectedSecondChildNodeCount: 1, // The origin child is rendered and doesn't have `[aria-selected]` @@ -175,9 +178,6 @@ describe('Resolver, when analyzing a tree that has two related events for the or simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text()) ) ).toYieldEqualTo(['2 registry']); - await expect( - simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').length) - ).toYieldEqualTo(1); }); }); describe('and when the related events button is clicked again', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 21b5a30ee9890c..037337fb2f868e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -152,9 +152,11 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an ]); }); it("should have the first node's ID in the query string", async () => { - await expect(simulator().map(() => simulator().queryStringValues())).toYieldEqualTo({ - selectedNode: [entityIDs.origin], - }); + await expect(simulator().map(() => simulator().historyLocationSearch)).toYieldEqualTo( + urlSearch(resolverComponentInstanceID, { + selectedEntityID: entityIDs.origin, + }) + ); }); describe('and when the node list link has been clicked', () => { beforeEach(async () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts new file mode 100644 index 00000000000000..26c25cfab2c215 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import { Simulator } from '../test_utilities/simulator'; +// Extend jest with a custom matcher +import '../test_utilities/extend_jest'; +import { urlSearch } from '../test_utilities/url_search'; + +let simulator: Simulator; +let databaseDocumentID: string; +let entityIDs: { origin: string; firstChild: string; secondChild: string }; + +// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances +const resolverComponentInstanceID = 'oldID'; + +describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => { + beforeEach(async () => { + // create a mock data access layer + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren(); + + // save a reference to the entity IDs exposed by the mock data layer + entityIDs = dataAccessLayerMetadata.entityIDs; + + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); + }); + + describe("when the second child node's first button has been clicked", () => { + beforeEach(async () => { + const node = await simulator.resolveWrapper(() => + simulator.processNodeElements({ entityID: entityIDs.secondChild }).find('button') + ); + if (node) { + // Click the first button under the second child element. + node.first().simulate('click'); + } + }); + const expectedSearch = urlSearch(resolverComponentInstanceID, { + selectedEntityID: 'secondChild', + }); + it(`should have a url search of ${expectedSearch}`, async () => { + await expect(simulator.map(() => simulator.historyLocationSearch)).toYieldEqualTo( + urlSearch(resolverComponentInstanceID, { selectedEntityID: 'secondChild' }) + ); + }); + describe('when the resolver component gets unmounted', () => { + beforeEach(() => { + simulator.unmount(); + }); + it('should have a history location search of `""`', async () => { + await expect(simulator.map(() => simulator.historyLocationSearch)).toYieldEqualTo(''); + }); + }); + describe('when the resolver component has its component instance ID changed', () => { + const newInstanceID = 'newID'; + beforeEach(() => { + simulator.resolverComponentInstanceID = newInstanceID; + }); + it('should have a history location search of `""`', async () => { + await expect(simulator.map(() => simulator.historyLocationSearch)).toYieldEqualTo(''); + }); + describe("when the user clicks the second child node's button again", () => { + beforeEach(async () => { + const node = await simulator.resolveWrapper(() => + simulator.processNodeElements({ entityID: entityIDs.secondChild }).find('button') + ); + if (node) { + // Click the first button under the second child element. + node.first().simulate('click'); + } + }); + it(`should have a url search of ${urlSearch(newInstanceID, { + selectedEntityID: 'secondChild', + })}`, async () => { + await expect(simulator.map(() => simulator.historyLocationSearch)).toYieldEqualTo( + urlSearch(newInstanceID, { selectedEntityID: 'secondChild' }) + ); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 5f1e5f18e575dd..32faeec043f2da 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -8,9 +8,9 @@ import React, { useContext, useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { useEffectOnce } from 'react-use'; import { EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useResolverQueryParamCleaner } from './use_resolver_query_params_cleaner'; import * as selectors from '../store/selectors'; import { EdgeLine } from './edge_line'; import { GraphControls } from './graph_controls'; @@ -18,7 +18,6 @@ import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; import { SymbolDefinitions, useResolverTheme } from './assets'; import { useStateSyncingActions } from './use_state_syncing_actions'; -import { useResolverQueryParams } from './use_resolver_query_params'; import { StyledMapContainer, StyledPanel, GraphContainer } from './styles'; import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context'; @@ -35,6 +34,7 @@ export const ResolverWithoutProviders = React.memo( { className, databaseDocumentID, resolverComponentInstanceID }: ResolverProps, refToForward ) { + useResolverQueryParamCleaner(); /** * This is responsible for dispatching actions that include any external data. * `databaseDocumentID` @@ -70,11 +70,6 @@ export const ResolverWithoutProviders = React.memo( const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); - const { cleanUpQueryParams } = useResolverQueryParams(); - - useEffectOnce(() => { - return () => cleanUpQueryParams(); - }); return ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_query_string_keys.ts b/x-pack/plugins/security_solution/public/resolver/view/use_query_string_keys.ts new file mode 100644 index 00000000000000..11f1a30db72fcf --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_query_string_keys.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useSelector } from 'react-redux'; +import * as selectors from '../store/selectors'; + +/** + * Get the query string keys used by this Resolver instance. + */ +export function useQueryStringKeys(): { idKey: string; eventKey: string } { + const resolverComponentInstanceID = useSelector(selectors.resolverComponentInstanceID); + const idKey: string = `resolver-${resolverComponentInstanceID}-id`; + const eventKey: string = `resolver-${resolverComponentInstanceID}-event`; + return { + idKey, + eventKey, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts index ed514a61d4e068..aa0851916a7b4e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts @@ -5,11 +5,8 @@ */ import { useCallback, useMemo } from 'react'; -// eslint-disable-next-line import/no-nodejs-modules -import querystring from 'querystring'; -import { useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; -import * as selectors from '../store/selectors'; +import { useQueryStringKeys } from './use_query_string_keys'; import { CrumbInfo } from './panels/panel_content_utilities'; export function useResolverQueryParams() { @@ -19,63 +16,40 @@ export function useResolverQueryParams() { */ const history = useHistory(); const urlSearch = useLocation().search; - const resolverComponentInstanceID = useSelector(selectors.resolverComponentInstanceID); - const uniqueCrumbIdKey: string = `resolver-${resolverComponentInstanceID}-id`; - const uniqueCrumbEventKey: string = `resolver-${resolverComponentInstanceID}-event`; + const { idKey, eventKey } = useQueryStringKeys(); const pushToQueryParams = useCallback( - (newCrumbs: CrumbInfo) => { - // Construct a new set of parameters from the current set (minus empty parameters) - // by assigning the new set of parameters provided in `newCrumbs` - const crumbsToPass = { - ...querystring.parse(urlSearch.slice(1)), - [uniqueCrumbIdKey]: newCrumbs.crumbId, - [uniqueCrumbEventKey]: newCrumbs.crumbEvent, - }; + (queryStringState: CrumbInfo) => { + const urlSearchParams = new URLSearchParams(urlSearch); - // If either was passed in as empty, remove it from the record - if (newCrumbs.crumbId === '') { - delete crumbsToPass[uniqueCrumbIdKey]; + urlSearchParams.set(idKey, queryStringState.crumbId); + urlSearchParams.set(eventKey, queryStringState.crumbEvent); + + // If either was passed in as empty, remove it + if (queryStringState.crumbId === '') { + urlSearchParams.delete(idKey); } - if (newCrumbs.crumbEvent === '') { - delete crumbsToPass[uniqueCrumbEventKey]; + if (queryStringState.crumbEvent === '') { + urlSearchParams.delete(eventKey); } - const relativeURL = { search: querystring.stringify(crumbsToPass) }; + const relativeURL = { search: urlSearchParams.toString() }; // We probably don't want to nuke the user's history with a huge // trail of these, thus `.replace` instead of `.push` return history.replace(relativeURL); }, - [history, urlSearch, uniqueCrumbIdKey, uniqueCrumbEventKey] + [history, urlSearch, idKey, eventKey] ); const queryParams: CrumbInfo = useMemo(() => { - const parsed = querystring.parse(urlSearch.slice(1)); - const crumbEvent = parsed[uniqueCrumbEventKey]; - const crumbId = parsed[uniqueCrumbIdKey]; - function valueForParam(param: string | string[]): string { - if (Array.isArray(param)) { - return param[0] || ''; - } - return param || ''; - } + const urlSearchParams = new URLSearchParams(urlSearch); return { - crumbEvent: valueForParam(crumbEvent), - crumbId: valueForParam(crumbId), + // Use `''` for backwards compatibility with deprecated code. + crumbEvent: urlSearchParams.get(eventKey) ?? '', + crumbId: urlSearchParams.get(idKey) ?? '', }; - }, [urlSearch, uniqueCrumbIdKey, uniqueCrumbEventKey]); - - const cleanUpQueryParams = () => { - const crumbsToPass = { - ...querystring.parse(urlSearch.slice(1)), - }; - delete crumbsToPass[uniqueCrumbIdKey]; - delete crumbsToPass[uniqueCrumbEventKey]; - const relativeURL = { search: querystring.stringify(crumbsToPass) }; - history.replace(relativeURL); - }; + }, [urlSearch, idKey, eventKey]); return { pushToQueryParams, queryParams, - cleanUpQueryParams, }; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params_cleaner.ts b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params_cleaner.ts new file mode 100644 index 00000000000000..a84eb0490aae2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params_cleaner.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useRef, useEffect } from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; + +import { useQueryStringKeys } from './use_query_string_keys'; +/** + * Cleanup any query string keys that were added by this Resolver instance. + * This works by having a React effect that just has behavior in the 'cleanup' function. + */ +export function useResolverQueryParamCleaner() { + /** + * Keep a reference to the current search value. This is used in the cleanup function. + * This value of useLocation().search isn't used directly since that would change and + * we only want the cleanup to run on unmount or when the resolverComponentInstanceID + * changes. + */ + const searchRef = useRef(); + searchRef.current = useLocation().search; + + const history = useHistory(); + + const { idKey, eventKey } = useQueryStringKeys(); + + useEffect(() => { + /** + * Keep track of the old query string keys so we can remove them. + */ + const oldIdKey = idKey; + const oldEventKey = eventKey; + /** + * When `idKey` or `eventKey` changes (such as when the `resolverComponentInstanceID` has changed) or when the component unmounts, remove any state from the query string. + */ + return () => { + /** + * This effect must not be invalidated when `search` changes. + */ + const urlSearchParams = new URLSearchParams(searchRef.current); + + /** + * Remove old keys from the url + */ + urlSearchParams.delete(oldIdKey); + urlSearchParams.delete(oldEventKey); + const relativeURL = { search: urlSearchParams.toString() }; + history.replace(relativeURL); + }; + }, [idKey, eventKey, history]); +} From 447854d992883f6dafc27743372701452ce7e8c4 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 13 Aug 2020 15:06:59 -0700 Subject: [PATCH 10/46] [Reporting/Functional] unskip pagination test (#74973) * [Reporting/Functional] unskip pagination test * change to js file for flaky test runner * fix ts --- .../__snapshots__/report_listing.test.tsx.snap | 3 --- .../reporting/public/components/report_listing.tsx | 1 - .../reporting_management/{index.ts => index.js} | 4 +--- .../apps/reporting_management/report_listing.ts | 14 ++++++++++---- 4 files changed, 11 insertions(+), 11 deletions(-) rename x-pack/test/functional/apps/reporting_management/{index.ts => index.js} (75%) diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap index 66c3aea8acc13c..ddba7842f11998 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap @@ -30,7 +30,6 @@ Array [ }, ] } - data-test-page={0} data-test-subj="reportJobListing" isSelectable={true} itemId="id" @@ -57,7 +56,6 @@ Array [ >
@@ -368,7 +366,6 @@ Array [ ,
diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 80ef9311fd0e54..afcae93a8db16f 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -513,7 +513,6 @@ class ReportListingUi extends Component { isSelectable={true} onChange={this.onTableChange} data-test-subj="reportJobListing" - data-test-page={this.state.page} /> {this.state.selectedJobs.length > 0 ? this.renderDeleteButton() : null} diff --git a/x-pack/test/functional/apps/reporting_management/index.ts b/x-pack/test/functional/apps/reporting_management/index.js similarity index 75% rename from x-pack/test/functional/apps/reporting_management/index.ts rename to x-pack/test/functional/apps/reporting_management/index.js index 8606c46053ab06..ef92e7d04ef0c0 100644 --- a/x-pack/test/functional/apps/reporting_management/index.ts +++ b/x-pack/test/functional/apps/reporting_management/index.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default ({ loadTestFile }: FtrProviderContext) => { +export default ({ loadTestFile }) => { describe('reporting management app', function () { this.tags('ciGroup7'); loadTestFile(require.resolve('./report_listing')); diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index 476f3e73d09238..ca5fb888e67e10 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -26,7 +26,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const security = getService('security'); const testSubjects = getService('testSubjects'); - const findInstance = getService('find'); const esArchiver = getService('esArchiver'); describe('Listing of Reports', function () { @@ -68,7 +67,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - it.skip('Paginates content', async () => { + it('Paginates historical reports', async () => { + // wait for first row of page 1 + await testSubjects.find('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + const previousButton = await testSubjects.find('pagination-button-previous'); // previous CAN NOT be clicked @@ -90,7 +92,9 @@ pdf\ndashboard\n2020-04-21 @ 07:00 PM\ntest_user\nCompleted at 2020-04-21 @ 07:0 // click page 2 await testSubjects.click('pagination-button-1'); - await findInstance.byCssSelector('[data-test-page="1"]'); + + // wait for first row of page 2 + await testSubjects.find('checkboxSelectRow-k9a9uc4x0gpe1457b16wthc8'); // previous CAN be clicked expect(await previousButton.getAttribute('disabled')).to.be(null); @@ -110,7 +114,9 @@ test_user\nCompleted at 2020-04-21 @ 06:55 PM - Max size reached\nreport2csv\n20 // click page 3 await testSubjects.click('pagination-button-2'); - await findInstance.byCssSelector('[data-test-page="2"]'); + + // wait for first row of page 3 + await testSubjects.find('checkboxSelectRow-k9a9p1840gpe1457b1ghfxw5'); // scan page 3 tableText = await getTableTextFromElement(await testSubjects.find('reportJobListing')); From 24c2e0a4523926eb1d144fec5fc830b7320929a5 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 13 Aug 2020 18:15:11 -0600 Subject: [PATCH 11/46] Remove degraded state from ES status service (#75007) --- src/core/server/elasticsearch/status.test.ts | 6 +++--- src/core/server/elasticsearch/status.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/server/elasticsearch/status.test.ts b/src/core/server/elasticsearch/status.test.ts index ef7ca7cd046088..5dfadba4c88b27 100644 --- a/src/core/server/elasticsearch/status.test.ts +++ b/src/core/server/elasticsearch/status.test.ts @@ -65,7 +65,7 @@ describe('calculateStatus', () => { }); }); - it('changes to degraded when isCompatible and warningNodes present', async () => { + it('changes to available with a differemnt message when isCompatible and warningNodes present', async () => { expect( await calculateStatus$( of({ @@ -81,7 +81,7 @@ describe('calculateStatus', () => { .pipe(take(2)) .toPromise() ).toEqual({ - level: ServiceStatusLevels.degraded, + level: ServiceStatusLevels.available, summary: 'Some nodes are a different version', meta: { incompatibleNodes: [], @@ -188,7 +188,7 @@ describe('calculateStatus', () => { "summary": "Incompatible with Elasticsearch", }, Object { - "level": degraded, + "level": available, "meta": Object { "incompatibleNodes": Array [], "warningNodes": Array [ diff --git a/src/core/server/elasticsearch/status.ts b/src/core/server/elasticsearch/status.ts index 1eaa338af12399..1be32d03c60cbd 100644 --- a/src/core/server/elasticsearch/status.ts +++ b/src/core/server/elasticsearch/status.ts @@ -55,7 +55,7 @@ export const calculateStatus$ = ( }; } else if (warningNodes.length > 0) { return { - level: ServiceStatusLevels.degraded, + level: ServiceStatusLevels.available, summary: // Message should always be present, but this is a safe fallback message ?? From 1632391f35d847d32245c502d0310849ae7b9322 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 13 Aug 2020 18:45:47 -0700 Subject: [PATCH 12/46] [Metrics UI] Remove TSVB dependency from Metrics Explorer APIs (#74804) * [Metrics UI] Remove TSVB dependency from Metrics Explorer APIs * Update x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_title.tsx Co-authored-by: Zacqary Adam Xeper * Update x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.ts Co-authored-by: Zacqary Adam Xeper * Update x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.ts Co-authored-by: Zacqary Adam Xeper * Update x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts Co-authored-by: Zacqary Adam Xeper * Update x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts Co-authored-by: Zacqary Adam Xeper * Update x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts Co-authored-by: Zacqary Adam Xeper * Fixing some names, changing some units * Reverting TSVB calculate_auto; fixing names in infra * Fixing translation names * Fixing typo Co-authored-by: Zacqary Adam Xeper Co-authored-by: Zacqary Adam Xeper --- x-pack/plugins/infra/common/constants.ts | 3 + x-pack/plugins/infra/common/http_api/index.ts | 1 + .../infra/common/http_api/metrics_api.ts | 97 +++++++++ .../infra/common/http_api/metrics_explorer.ts | 2 + .../aws_ec2/metrics/snapshot/cpu.ts | 4 +- .../metrics/snapshot/disk_io_read_bytes.ts | 4 +- .../metrics/snapshot/disk_io_write_bytes.ts | 4 +- .../aws_ec2/metrics/snapshot/rx.ts | 4 +- .../aws_ec2/metrics/snapshot/tx.ts | 4 +- .../aws_rds/metrics/snapshot/cpu.ts | 4 +- .../snapshot/rds_active_transactions.ts | 4 +- .../metrics/snapshot/rds_connections.ts | 4 +- .../aws_rds/metrics/snapshot/rds_latency.ts | 4 +- .../metrics/snapshot/rds_queries_executed.ts | 4 +- .../aws_s3/metrics/snapshot/s3_bucket_size.ts | 4 +- .../metrics/snapshot/s3_download_bytes.ts | 4 +- .../metrics/snapshot/s3_number_of_objects.ts | 4 +- .../metrics/snapshot/s3_total_requests.ts | 4 +- .../metrics/snapshot/s3_upload_bytes.ts | 4 +- .../metrics/snapshot/sqs_messages_delayed.ts | 4 +- .../metrics/snapshot/sqs_messages_empty.ts | 4 +- .../metrics/snapshot/sqs_messages_sent.ts | 4 +- .../metrics/snapshot/sqs_messages_visible.ts | 4 +- .../metrics/snapshot/sqs_oldest_message.ts | 4 +- .../container/metrics/snapshot/cpu.ts | 4 +- .../container/metrics/snapshot/memory.ts | 6 +- .../host/metrics/snapshot/cpu.ts | 4 +- .../host/metrics/snapshot/load.ts | 4 +- .../host/metrics/snapshot/log_rate.ts | 4 +- .../host/metrics/snapshot/memory.ts | 4 +- .../infra/common/inventory_models/index.ts | 4 +- .../pod/metrics/snapshot/cpu.ts | 4 +- .../pod/metrics/snapshot/memory.ts | 4 +- .../shared/metrics/snapshot/count.ts | 4 +- .../metrics/snapshot/network_traffic.ts | 4 +- .../network_traffic_with_interfaces.ts | 4 +- .../shared/metrics/snapshot/rate.ts | 4 +- .../infra/common/inventory_models/types.ts | 55 +++-- .../indices_configuration_panel.tsx | 3 +- .../components/chart_title.tsx | 7 +- .../components/helpers/get_metric_id.ts | 3 - .../infra/server/lib/metrics/constants.ts | 16 ++ .../plugins/infra/server/lib/metrics/index.ts | 113 ++++++++++ ...stogram_buckets_to_timeseries.test.ts.snap | 193 ++++++++++++++++++ .../create_aggregations.test.ts.snap | 87 ++++++++ .../create_metrics_aggregations.test.ts.snap | 23 +++ .../calculate_auto.test.ts | 28 +++ .../calculate_bucket_size/calculate_auto.ts | 88 ++++++++ .../calculate_bucket_size.test.ts | 53 +++++ .../lib/calculate_bucket_size/index.ts | 89 ++++++++ .../interval_regex.test.ts | 81 ++++++++ .../calculate_bucket_size/interval_regex.ts | 10 + .../unit_to_seconds.test.ts | 132 ++++++++++++ .../calculate_bucket_size/unit_to_seconds.ts | 68 ++++++ .../calculate_date_histogram_offset.test.ts | 20 ++ .../lib/calculate_date_histogram_offset.ts | 17 ++ ...rt_histogram_buckets_to_timeseries.test.ts | 120 +++++++++++ ...convert_histogram_buckets_to_timeseries.ts | 93 +++++++++ .../metrics/lib/create_aggregations.test.ts | 45 ++++ .../lib/metrics/lib/create_aggregations.ts | 45 ++++ .../lib/create_metrics_aggregations.test.ts | 36 ++++ .../lib/create_metrics_aggregations.ts | 15 ++ .../plugins/infra/server/lib/metrics/types.ts | 72 +++++++ .../create_timerange_with_interval.ts | 6 +- .../server/lib/snapshot/query_helpers.ts | 10 +- .../infra/server/lib/sources/defaults.ts | 11 +- .../server/routes/metrics_explorer/index.ts | 64 ++++-- ...nvert_metric_to_metrics_api_metric.test.ts | 87 ++++++++ .../convert_metric_to_metrics_api_metric.ts | 62 ++++++ ...ert_request_to_metrics_api_options.test.ts | 123 +++++++++++ .../convert_request_to_metrics_api_options.ts | 58 ++++++ .../lib/create_metrics_model.ts | 97 --------- .../lib/find_interval_for_metrics.ts | 52 +++++ .../metrics_explorer/lib/get_groupings.ts | 150 -------------- .../lib/populate_series_with_tsvb_data.ts | 162 --------------- .../lib/query_total_groupings.ts | 59 ++++++ .../metrics_explorer/lib/transform_series.ts | 24 +++ .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- .../apis/metrics_ui/metrics_explorer.ts | 6 +- 80 files changed, 2190 insertions(+), 534 deletions(-) create mode 100644 x-pack/plugins/infra/common/http_api/metrics_api.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/constants.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/index.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/convert_histogram_buckets_to_timeseries.test.ts.snap create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_metrics_aggregations.test.ts.snap create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_bucket_size.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/index.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.ts create mode 100644 x-pack/plugins/infra/server/lib/metrics/types.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.test.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.ts delete mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts delete mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts delete mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts create mode 100644 x-pack/plugins/infra/server/routes/metrics_explorer/lib/transform_series.ts diff --git a/x-pack/plugins/infra/common/constants.ts b/x-pack/plugins/infra/common/constants.ts index 65dcb2e43c6f7f..ba8e421cbf32cb 100644 --- a/x-pack/plugins/infra/common/constants.ts +++ b/x-pack/plugins/infra/common/constants.ts @@ -5,3 +5,6 @@ */ export const DEFAULT_SOURCE_ID = 'default'; +export const METRICS_INDEX_PATTERN = 'metrics-*,metricbeat-*'; +export const LOGS_INDEX_PATTERN = 'logs-*,filebeat-*,kibana_sample_data_logs*'; +export const TIMESTAMP_FIELD = '@timestamp'; diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts index 326daa93de33ac..9ec8bf5231066e 100644 --- a/x-pack/plugins/infra/common/http_api/index.ts +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -8,3 +8,4 @@ export * from './log_analysis'; export * from './metadata_api'; export * from './log_entries'; export * from './metrics_explorer'; +export * from './metrics_api'; diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts new file mode 100644 index 00000000000000..7436566f039ca2 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { MetricsUIAggregationRT } from '../inventory_models/types'; +import { afterKeyObjectRT } from './metrics_explorer'; + +export const MetricsAPITimerangeRT = rt.type({ + field: rt.string, + from: rt.number, + to: rt.number, + interval: rt.string, +}); + +const groupByRT = rt.union([rt.string, rt.null, rt.undefined]); + +export const MetricsAPIMetricRT = rt.type({ + id: rt.string, + aggregations: MetricsUIAggregationRT, +}); + +export const MetricsAPIRequestRT = rt.intersection([ + rt.type({ + timerange: MetricsAPITimerangeRT, + indexPattern: rt.string, + metrics: rt.array(MetricsAPIMetricRT), + }), + rt.partial({ + groupBy: rt.array(groupByRT), + afterKey: rt.union([rt.null, afterKeyObjectRT]), + limit: rt.union([rt.number, rt.null, rt.undefined]), + filters: rt.array(rt.object), + forceInterval: rt.boolean, + dropLastBucket: rt.boolean, + alignDataToEnd: rt.boolean, + }), +]); + +export const MetricsAPIPageInfoRT = rt.type({ + afterKey: rt.union([rt.null, afterKeyObjectRT, rt.undefined]), + interval: rt.number, +}); + +export const MetricsAPIColumnTypeRT = rt.keyof({ + date: null, + number: null, + string: null, +}); + +export const MetricsAPIColumnRT = rt.type({ + name: rt.string, + type: MetricsAPIColumnTypeRT, +}); + +export const MetricsAPIRowRT = rt.intersection([ + rt.type({ + timestamp: rt.number, + }), + rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])), +]); + +export const MetricsAPISeriesRT = rt.intersection([ + rt.type({ + id: rt.string, + columns: rt.array(MetricsAPIColumnRT), + rows: rt.array(MetricsAPIRowRT), + }), + rt.partial({ + keys: rt.array(rt.string), + }), +]); + +export const MetricsAPIResponseRT = rt.type({ + series: rt.array(MetricsAPISeriesRT), + info: MetricsAPIPageInfoRT, +}); + +export type MetricsAPITimerange = rt.TypeOf; + +export type MetricsAPIColumnType = rt.TypeOf; + +export type MetricsAPIMetric = rt.TypeOf; + +export type MetricsAPIPageInfo = rt.TypeOf; + +export type MetricsAPIColumn = rt.TypeOf; + +export type MetricsAPIRow = rt.TypeOf; + +export type MetricsAPISeries = rt.TypeOf; + +export type MetricsAPIRequest = rt.TypeOf; + +export type MetricsAPIResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts index 0f63b8d275e653..c5776e0b0ced16 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -108,6 +108,8 @@ export const metricsExplorerResponseRT = rt.type({ pageInfo: metricsExplorerPageInfoRT, }); +export type AfterKey = rt.TypeOf; + export type MetricsExplorerAggregation = rt.TypeOf; export type MetricsExplorerColumnType = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts index 483d9de784919d..ea6db373eda031 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const cpu: SnapshotModel = { +export const cpu: MetricsUIAggregation = { cpu_avg: { avg: { field: 'aws.ec2.cpu.total.pct', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts index 48e4a9eb59fadc..89b131f9c2a8ca 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const diskIOReadBytes: SnapshotModel = { +export const diskIOReadBytes: MetricsUIAggregation = { diskIOReadBytes: { avg: { field: 'aws.ec2.diskio.read.bytes_per_sec', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts index deadaa8c4a7768..e52380fbddd331 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const diskIOWriteBytes: SnapshotModel = { +export const diskIOWriteBytes: MetricsUIAggregation = { diskIOWriteBytes: { avg: { field: 'aws.ec2.diskio.write.bytes_per_sec', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts index 2b857ce9b338a0..ddb67707446990 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const rx: SnapshotModel = { +export const rx: MetricsUIAggregation = { rx: { avg: { field: 'aws.ec2.network.in.bytes_per_sec', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts index 63c9da8ea18882..cf3bb070c8ec83 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const tx: SnapshotModel = { +export const tx: MetricsUIAggregation = { tx: { avg: { field: 'aws.ec2.network.in.bytes_per_sec', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts index e277b3b11958b7..4825fb26aa6771 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const cpu: SnapshotModel = { +export const cpu: MetricsUIAggregation = { cpu_avg: { avg: { field: 'aws.rds.cpu.total.pct', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts index be3dba100ba299..50c89a51a32da7 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const rdsActiveTransactions: SnapshotModel = { +export const rdsActiveTransactions: MetricsUIAggregation = { rdsActiveTransactions: { avg: { field: 'aws.rds.transactions.active', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts index c7855d5548eeab..4f8ab3bec324b4 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const rdsConnections: SnapshotModel = { +export const rdsConnections: MetricsUIAggregation = { rdsConnections: { avg: { field: 'aws.rds.database_connections', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts index 2997b54d2f92e9..1682cab04cd7af 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const rdsLatency: SnapshotModel = { +export const rdsLatency: MetricsUIAggregation = { rdsLatency: { avg: { field: 'aws.rds.latency.dml', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts index 18e6538fb1e1e3..7672d17f9d048d 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const rdsQueriesExecuted: SnapshotModel = { +export const rdsQueriesExecuted: MetricsUIAggregation = { rdsQueriesExecuted: { avg: { field: 'aws.rds.queries', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts index a99753a39c97ce..b087d63f958637 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const s3BucketSize: SnapshotModel = { +export const s3BucketSize: MetricsUIAggregation = { s3BucketSize: { max: { field: 'aws.s3_daily_storage.bucket.size.bytes', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts index a0b23dadee37aa..393f09b7340edd 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const s3DownloadBytes: SnapshotModel = { +export const s3DownloadBytes: MetricsUIAggregation = { s3DownloadBytes: { max: { field: 'aws.s3_request.downloaded.bytes', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts index 29162a59db47a8..4e360c93ae26cd 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const s3NumberOfObjects: SnapshotModel = { +export const s3NumberOfObjects: MetricsUIAggregation = { s3NumberOfObjects: { max: { field: 'aws.s3_daily_storage.number_of_objects', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts index bc57c6eb382348..87fcd27efa1ec2 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const s3TotalRequests: SnapshotModel = { +export const s3TotalRequests: MetricsUIAggregation = { s3TotalRequests: { max: { field: 'aws.s3_request.requests.total', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts index 977d73254c3cd4..c5b7f50a39051f 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const s3UploadBytes: SnapshotModel = { +export const s3UploadBytes: MetricsUIAggregation = { s3UploadBytes: { max: { field: 'aws.s3_request.uploaded.bytes', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts index 679f86671725e7..630c7946e44780 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const sqsMessagesDelayed: SnapshotModel = { +export const sqsMessagesDelayed: MetricsUIAggregation = { sqsMessagesDelayed: { max: { field: 'aws.sqs.messages.delayed', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts index d80a3f3451e1d8..15d1cf9b5df81a 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const sqsMessagesEmpty: SnapshotModel = { +export const sqsMessagesEmpty: MetricsUIAggregation = { sqsMessagesEmpty: { max: { field: 'aws.sqs.messages.not_visible', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts index 3d6934bf3da85f..fd6ea2ee53ed26 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const sqsMessagesSent: SnapshotModel = { +export const sqsMessagesSent: MetricsUIAggregation = { sqsMessagesSent: { max: { field: 'aws.sqs.messages.sent', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts index 1a78c50cd7949f..26a1084c63e46e 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const sqsMessagesVisible: SnapshotModel = { +export const sqsMessagesVisible: MetricsUIAggregation = { sqsMessagesVisible: { avg: { field: 'aws.sqs.messages.visible', diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts index ae780069c8ca1f..13145793192643 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const sqsOldestMessage: SnapshotModel = { +export const sqsOldestMessage: MetricsUIAggregation = { sqsOldestMessage: { max: { field: 'aws.sqs.oldest_message_age.sec', diff --git a/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/cpu.ts b/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/cpu.ts index a6c25ee260cacd..d5a6229ff56f1f 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/cpu.ts +++ b/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/cpu.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const cpu: SnapshotModel = { +export const cpu: MetricsUIAggregation = { cpu: { avg: { field: 'docker.cpu.total.pct', diff --git a/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/memory.ts b/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/memory.ts index 30df0ebbaa1d47..693f9569edd776 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/memory.ts +++ b/x-pack/plugins/infra/common/inventory_models/container/metrics/snapshot/memory.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const memory: SnapshotModel = { memory: { avg: { field: 'docker.memory.usage.pct' } } }; +export const memory: MetricsUIAggregation = { + memory: { avg: { field: 'docker.memory.usage.pct' } }, +}; diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/cpu.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/cpu.ts index fa43acb8d6108c..047593d65c5462 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/cpu.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/cpu.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const cpu: SnapshotModel = { +export const cpu: MetricsUIAggregation = { cpu_user: { avg: { field: 'system.cpu.user.pct', diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/load.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/load.ts index 803fb2664ad27c..d42ed590f1bd81 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/load.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/load.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const load: SnapshotModel = { load: { avg: { field: 'system.load.5' } } }; +export const load: MetricsUIAggregation = { load: { avg: { field: 'system.load.5' } } }; diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/log_rate.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/log_rate.ts index 658111bd076769..5e516d12eadf86 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/log_rate.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/log_rate.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const logRate: SnapshotModel = { +export const logRate: MetricsUIAggregation = { count: { bucket_script: { buckets_path: { count: '_count' }, diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/memory.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/memory.ts index cb08a9eaebb3b7..adfe831fa658d1 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/memory.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/snapshot/memory.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const memory: SnapshotModel = { +export const memory: MetricsUIAggregation = { memory: { avg: { field: 'system.memory.actual.used.pct' } }, }; diff --git a/x-pack/plugins/infra/common/inventory_models/index.ts b/x-pack/plugins/infra/common/inventory_models/index.ts index 1ddf92516c409c..84bdb7887b1d14 100644 --- a/x-pack/plugins/infra/common/inventory_models/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/index.ts @@ -51,9 +51,9 @@ const getFieldByType = (type: InventoryItemType, fields: InventoryFields) => { } }; -export const findInventoryFields = (type: InventoryItemType, fields: InventoryFields) => { +export const findInventoryFields = (type: InventoryItemType, fields?: InventoryFields) => { const inventoryModel = findInventoryModel(type); - if (LEGACY_TYPES.includes(type)) { + if (fields && LEGACY_TYPES.includes(type)) { const id = getFieldByType(type, fields) || inventoryModel.fields.id; return { ...inventoryModel.fields, diff --git a/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/cpu.ts b/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/cpu.ts index d5979d455f0bf2..18292b9cf326d6 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/cpu.ts +++ b/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/cpu.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const cpu: SnapshotModel = { +export const cpu: MetricsUIAggregation = { cpu_with_limit: { avg: { field: 'kubernetes.pod.cpu.usage.limit.pct', diff --git a/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/memory.ts b/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/memory.ts index 28a71d9b0275a2..9b78d625b73d63 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/memory.ts +++ b/x-pack/plugins/infra/common/inventory_models/pod/metrics/snapshot/memory.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const memory: SnapshotModel = { +export const memory: MetricsUIAggregation = { memory: { avg: { field: 'kubernetes.pod.memory.usage.node.pct' } }, }; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/count.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/count.ts index ed8398a5d4a77d..428d542542c4c1 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/count.ts +++ b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/count.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const count: SnapshotModel = { +export const count: MetricsUIAggregation = { count: { bucket_script: { buckets_path: { count: '_count' }, diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic.ts index 37e90a6416ba75..28c866ff44b985 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic.ts +++ b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const networkTraffic = (id: string, field: string): SnapshotModel => { +export const networkTraffic = (id: string, field: string): MetricsUIAggregation => { return { [`${id}_max`]: { max: { field } }, [`${id}_deriv`]: { diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic_with_interfaces.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic_with_interfaces.ts index 1ba5cf037e7087..3ba8f2eafcc276 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic_with_interfaces.ts +++ b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/network_traffic_with_interfaces.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; export const networkTrafficWithInterfaces = ( id: string, metricField: string, interfaceField: string -): SnapshotModel => ({ +): MetricsUIAggregation => ({ [`${id}_interfaces`]: { terms: { field: interfaceField }, aggregations: { diff --git a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/rate.ts b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/rate.ts index e1c7c7df526281..312d4e80629882 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/rate.ts +++ b/x-pack/plugins/infra/common/inventory_models/shared/metrics/snapshot/rate.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotModel } from '../../../types'; +import { MetricsUIAggregation } from '../../../types'; -export const rate = (id: string, field: string): SnapshotModel => { +export const rate = (id: string, field: string): MetricsUIAggregation => { return { [`${id}_max`]: { max: { field } }, [`${id}_deriv`]: { diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 2c6432c3e52862..570220bbc7aa52 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -224,7 +224,7 @@ export type TSVBMetricModelCreator = ( interval: string ) => TSVBMetricModel; -export const SnapshotModelMetricAggRT = rt.record( +export const ESBasicMetricAggRT = rt.record( rt.string, rt.union([ rt.undefined, @@ -234,7 +234,21 @@ export const SnapshotModelMetricAggRT = rt.record( ]) ); -export const SnapshotModelBucketScriptRT = rt.type({ +export const ESPercentileAggRT = rt.type({ + percentiles: rt.type({ + field: rt.string, + percents: rt.array(rt.number), + }), +}); + +export const ESCaridnalityAggRT = rt.type({ + cardinality: rt.partial({ + field: rt.string, + script: rt.string, + }), +}); + +export const ESBucketScriptAggRT = rt.type({ bucket_script: rt.intersection([ rt.type({ buckets_path: rt.record(rt.string, rt.union([rt.undefined, rt.string])), @@ -247,13 +261,13 @@ export const SnapshotModelBucketScriptRT = rt.type({ ]), }); -export const SnapshotModelCumulativeSumRT = rt.type({ +export const ESCumulativeSumAggRT = rt.type({ cumulative_sum: rt.type({ buckets_path: rt.string, }), }); -export const SnapshotModelDerivativeRT = rt.type({ +export const ESDerivativeAggRT = rt.type({ derivative: rt.type({ buckets_path: rt.string, gap_policy: rt.keyof({ skip: null, insert_zeros: null }), @@ -261,7 +275,7 @@ export const SnapshotModelDerivativeRT = rt.type({ }), }); -export const SnapshotModelSumBucketRT = rt.type({ +export const ESSumBucketAggRT = rt.type({ sum_bucket: rt.type({ buckets_path: rt.string, }), @@ -269,32 +283,31 @@ export const SnapshotModelSumBucketRT = rt.type({ interface SnapshotTermsWithAggregation { terms: { field: string }; - aggregations: SnapshotModel; + aggregations: MetricsUIAggregation; } -export const SnapshotTermsWithAggregationRT: rt.Type = rt.recursion( +export const ESTermsWithAggregationRT: rt.Type = rt.recursion( 'SnapshotModelRT', () => rt.type({ terms: rt.type({ field: rt.string }), - aggregations: SnapshotModelRT, + aggregations: MetricsUIAggregationRT, }) ); -export const SnapshotModelAggregationRT = rt.union([ - SnapshotModelMetricAggRT, - SnapshotModelBucketScriptRT, - SnapshotModelCumulativeSumRT, - SnapshotModelDerivativeRT, - SnapshotModelSumBucketRT, - SnapshotTermsWithAggregationRT, +export const ESAggregationRT = rt.union([ + ESBasicMetricAggRT, + ESPercentileAggRT, + ESBucketScriptAggRT, + ESCumulativeSumAggRT, + ESDerivativeAggRT, + ESSumBucketAggRT, + ESTermsWithAggregationRT, + ESCaridnalityAggRT, ]); -export const SnapshotModelRT = rt.record( - rt.string, - rt.union([rt.undefined, SnapshotModelAggregationRT]) -); -export type SnapshotModel = rt.TypeOf; +export const MetricsUIAggregationRT = rt.record(rt.string, ESAggregationRT); +export type MetricsUIAggregation = rt.TypeOf; export const SnapshotMetricTypeRT = rt.keyof({ count: null, @@ -327,7 +340,7 @@ export type SnapshotMetricType = rt.TypeOf; export interface InventoryMetrics { tsvb: { [name: string]: TSVBMetricModelCreator }; - snapshot: { [name: string]: SnapshotModel }; + snapshot: { [name: string]: MetricsUIAggregation }; defaultSnapshot: SnapshotMetricType; /** This is used by the inventory view to calculate the appropriate amount of time for the metrics detail page. Some metris like awsS3 require multiple days where others like host only need an hour.*/ defaultTimeRangeInSeconds: number; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index e9817331ace937..d5fafe3ce41a01 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -16,6 +16,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; +import { METRICS_INDEX_PATTERN } from '../../../common/constants'; import { InputFieldProps } from './input_fields'; interface IndicesConfigurationPanelProps { @@ -63,7 +64,7 @@ export const IndicesConfigurationPanel = ({ id="xpack.infra.sourceConfiguration.metricIndicesRecommendedValue" defaultMessage="The recommended value is {defaultValue}" values={{ - defaultValue: metrics-*,metricbeat-*, + defaultValue: {METRICS_INDEX_PATTERN}, }} /> } diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_title.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_title.tsx index e756c3bc393ce5..c40aef5888ad7b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_title.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_title.tsx @@ -6,12 +6,17 @@ import React, { Fragment } from 'react'; import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { MetricsExplorerSeries } from '../../../../../common/http_api'; interface Props { series: MetricsExplorerSeries; } +const ALL_TITLE = i18n.translate('xpack.infra.metricsExplorer.everything', { + defaultMessage: 'Everything', +}); + export const ChartTitle = ({ series }: Props) => { if (series.keys != null) { const { keys } = series; @@ -21,7 +26,7 @@ export const ChartTitle = ({ series }: Props) => { i ? 'subdued' : 'default'}> - {name} + {name === '*' ? ALL_TITLE : name} {keys.length - 1 > i && ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_metric_id.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_metric_id.ts index 35ca2561b0862d..17548de9b2e784 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_metric_id.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_metric_id.ts @@ -7,8 +7,5 @@ import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; export const getMetricId = (metric: MetricsExplorerOptionsMetric, index: string | number) => { - if (['p95', 'p99'].includes(metric.aggregation)) { - return `metric_${index}:percentile_0`; - } return `metric_${index}`; }; diff --git a/x-pack/plugins/infra/server/lib/metrics/constants.ts b/x-pack/plugins/infra/server/lib/metrics/constants.ts new file mode 100644 index 00000000000000..590eaf5605c728 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const EMPTY_RESPONSE = { + series: [ + { + id: '*', + keys: ['*'], + columns: [], + rows: [], + }, + ], + info: { total: 0, afterKey: null, interval: 0 }, +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts new file mode 100644 index 00000000000000..183254a0486a2e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { set } from '@elastic/safer-lodash-set'; +import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { MetricsAPIRequest, MetricsAPIResponse, afterKeyObjectRT } from '../../../common/http_api'; +import { + ESSearchClient, + GroupingResponseRT, + MetricsESResponse, + HistogramResponseRT, +} from './types'; +import { EMPTY_RESPONSE } from './constants'; +import { createAggregations } from './lib/create_aggregations'; +import { convertHistogramBucketsToTimeseries } from './lib/convert_histogram_buckets_to_timeseries'; +import { calculateBucketSize } from './lib/calculate_bucket_size'; + +export const query = async ( + search: ESSearchClient, + options: MetricsAPIRequest +): Promise => { + const hasGroupBy = Array.isArray(options.groupBy) && options.groupBy.length > 0; + const filter: Array> = [ + { + range: { + [options.timerange.field]: { + gte: options.timerange.from, + lte: options.timerange.to, + format: 'epoch_millis', + }, + }, + }, + ...(options.groupBy?.map((field) => ({ exists: { field } })) ?? []), + ]; + const params = { + allowNoIndices: true, + ignoreUnavailable: true, + index: options.indexPattern, + body: { + size: 0, + query: { bool: { filter } }, + aggs: { ...createAggregations(options) }, + }, + }; + + if (hasGroupBy) { + if (options.afterKey) { + if (afterKeyObjectRT.is(options.afterKey)) { + set(params, 'body.aggs.groupings.composite.after', options.afterKey); + } else { + set(params, 'body.aggs.groupings.composite.after', { groupBy0: options.afterKey }); + } + } + } + + if (options.filters) { + params.body.query.bool.filter = [...params.body.query.bool.filter, ...options.filters]; + } + + const response = await search<{}, MetricsESResponse>(params); + + if (response.hits.total.value === 0) { + return EMPTY_RESPONSE; + } + + if (!response.aggregations) { + throw new Error('Aggregations should be present.'); + } + + const { bucketSize } = calculateBucketSize(options.timerange); + + if (hasGroupBy && GroupingResponseRT.is(response.aggregations)) { + const { groupings } = response.aggregations; + const { after_key: afterKey } = groupings; + const limit = options.limit || 9; + const returnAfterKey = afterKey && groupings.buckets.length === limit ? true : false; + return { + series: groupings.buckets.map((bucket) => { + const keys = Object.values(bucket.key); + return convertHistogramBucketsToTimeseries(keys, options, bucket.histogram.buckets); + }), + info: { + afterKey: returnAfterKey ? afterKey : null, + interval: bucketSize, + }, + }; + } else if (hasGroupBy) { + ThrowReporter.report(GroupingResponseRT.decode(response.aggregations)); + } + + if (HistogramResponseRT.is(response.aggregations)) { + return { + series: [ + convertHistogramBucketsToTimeseries( + ['*'], + options, + response.aggregations.histogram.buckets + ), + ], + info: { + afterKey: null, + interval: bucketSize, + }, + }; + } else { + ThrowReporter.report(HistogramResponseRT.decode(response.aggregations)); + } + + throw new Error('Elasticsearch responsed with an unrecoginzed format.'); +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/convert_histogram_buckets_to_timeseries.test.ts.snap b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/convert_histogram_buckets_to_timeseries.test.ts.snap new file mode 100644 index 00000000000000..9f4cd67c07b6b5 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/convert_histogram_buckets_to_timeseries.test.ts.snap @@ -0,0 +1,193 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`convertHistogramBucketsToTimeseies(keys, options, buckets) should drop the last bucket 1`] = ` +Object { + "columns": Array [ + Object { + "name": "timestamp", + "type": "date", + }, + Object { + "name": "metric_0", + "type": "number", + }, + ], + "id": "example-0", + "keys": Array [ + "example-0", + ], + "rows": Array [ + Object { + "metric_0": 1, + "timestamp": 1577836800000, + }, + Object { + "metric_0": 1, + "timestamp": 1577836860000, + }, + Object { + "metric_0": 1, + "timestamp": 1577836920000, + }, + ], +} +`; + +exports[`convertHistogramBucketsToTimeseies(keys, options, buckets) should just work 1`] = ` +Object { + "columns": Array [ + Object { + "name": "timestamp", + "type": "date", + }, + Object { + "name": "metric_0", + "type": "number", + }, + ], + "id": "example-0", + "keys": Array [ + "example-0", + ], + "rows": Array [ + Object { + "metric_0": 1, + "timestamp": 1577836800000, + }, + Object { + "metric_0": 1, + "timestamp": 1577836860000, + }, + Object { + "metric_0": 1, + "timestamp": 1577836920000, + }, + Object { + "metric_0": null, + "timestamp": 1577836920000, + }, + ], +} +`; + +exports[`convertHistogramBucketsToTimeseies(keys, options, buckets) should return empty timeseries for empty metrics 1`] = ` +Object { + "columns": Array [], + "id": "example-0", + "keys": Array [ + "example-0", + ], + "rows": Array [], +} +`; + +exports[`convertHistogramBucketsToTimeseies(keys, options, buckets) should work with keyed percentiles 1`] = ` +Object { + "columns": Array [ + Object { + "name": "timestamp", + "type": "date", + }, + Object { + "name": "metric_0", + "type": "number", + }, + ], + "id": "example-0", + "keys": Array [ + "example-0", + ], + "rows": Array [ + Object { + "metric_0": 4, + "timestamp": 1577836800000, + }, + Object { + "metric_0": 4, + "timestamp": 1577836860000, + }, + Object { + "metric_0": 4, + "timestamp": 1577836920000, + }, + Object { + "metric_0": 4, + "timestamp": 1577836920000, + }, + ], +} +`; + +exports[`convertHistogramBucketsToTimeseies(keys, options, buckets) should work with normalized_values 1`] = ` +Object { + "columns": Array [ + Object { + "name": "timestamp", + "type": "date", + }, + Object { + "name": "metric_0", + "type": "number", + }, + ], + "id": "example-0", + "keys": Array [ + "example-0", + ], + "rows": Array [ + Object { + "metric_0": 2, + "timestamp": 1577836800000, + }, + Object { + "metric_0": 2, + "timestamp": 1577836860000, + }, + Object { + "metric_0": 2, + "timestamp": 1577836920000, + }, + Object { + "metric_0": null, + "timestamp": 1577836920000, + }, + ], +} +`; + +exports[`convertHistogramBucketsToTimeseies(keys, options, buckets) should work with percentiles 1`] = ` +Object { + "columns": Array [ + Object { + "name": "timestamp", + "type": "date", + }, + Object { + "name": "metric_0", + "type": "number", + }, + ], + "id": "example-0", + "keys": Array [ + "example-0", + ], + "rows": Array [ + Object { + "metric_0": 3, + "timestamp": 1577836800000, + }, + Object { + "metric_0": 3, + "timestamp": 1577836860000, + }, + Object { + "metric_0": 3, + "timestamp": 1577836920000, + }, + Object { + "metric_0": 3, + "timestamp": 1577836920000, + }, + ], +} +`; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap new file mode 100644 index 00000000000000..d2d90914eced5c --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`createAggregations(options) should return add offset to histogram 1`] = ` +Object { + "histogram": Object { + "aggregations": Object { + "metric_0": Object { + "avg": Object { + "field": "system.cpu.user.pct", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1577838720000, + "min": 1577835120000, + }, + "field": "@timestamp", + "fixed_interval": "1m", + "offset": "-60s", + }, + }, +} +`; + +exports[`createAggregations(options) should return groupings aggregation with groupBy 1`] = ` +Object { + "groupings": Object { + "aggs": Object { + "histogram": Object { + "aggregations": Object { + "metric_0": Object { + "avg": Object { + "field": "system.cpu.user.pct", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1577840400000, + "min": 1577836800000, + }, + "field": "@timestamp", + "fixed_interval": "1m", + "offset": "0s", + }, + }, + }, + "composite": Object { + "size": 20, + "sources": Array [ + Object { + "groupBy0": Object { + "terms": Object { + "field": "host.name", + "order": "asc", + }, + }, + }, + ], + }, + }, +} +`; + +exports[`createAggregations(options) should return just histogram aggregation without groupBy 1`] = ` +Object { + "histogram": Object { + "aggregations": Object { + "metric_0": Object { + "avg": Object { + "field": "system.cpu.user.pct", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1577840400000, + "min": 1577836800000, + }, + "field": "@timestamp", + "fixed_interval": "1m", + "offset": "0s", + }, + }, +} +`; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_metrics_aggregations.test.ts.snap b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_metrics_aggregations.test.ts.snap new file mode 100644 index 00000000000000..bbfe7e9cf0f9f1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_metrics_aggregations.test.ts.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`createMetricsAggregations(options) should just work 1`] = ` +Object { + "metric_0": Object { + "avg": Object { + "field": "system.cpu.user.pct", + }, + }, + "metric_1": Object { + "derivative": Object { + "buckets_path": "metric_1_max", + "gap_policy": "skip", + "unit": "1s", + }, + }, + "metric_1_max": Object { + "max": Object { + "field": "system.network.in.bytes", + }, + }, +} +`; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.test.ts new file mode 100644 index 00000000000000..a2f8a08afc3034 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { calculateAuto } from './calculate_auto'; +import moment, { isDuration } from 'moment'; + +describe('calculateAuto.near(bucket, duration)', () => { + it('should calculate the bucket size for 15 minutes', () => { + const bucketSizeDuration = calculateAuto.near(100, moment.duration(15, 'minutes')); + expect(bucketSizeDuration).not.toBeUndefined(); + expect(isDuration(bucketSizeDuration)).toBeTruthy(); + expect(bucketSizeDuration!.asSeconds()).toBe(10); + }); + it('should calculate the bucket size for an hour', () => { + const bucketSizeDuration = calculateAuto.near(100, moment.duration(1, 'hour')); + expect(bucketSizeDuration).not.toBeUndefined(); + expect(isDuration(bucketSizeDuration)).toBeTruthy(); + expect(bucketSizeDuration!.asSeconds()).toBe(30); + }); + it('should calculate the bucket size for a day', () => { + const bucketSizeDuration = calculateAuto.near(100, moment.duration(1, 'day')); + expect(bucketSizeDuration).not.toBeUndefined(); + expect(isDuration(bucketSizeDuration)).toBeTruthy(); + expect(bucketSizeDuration!.asMinutes()).toBe(10); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.ts new file mode 100644 index 00000000000000..00f321b25016d1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_auto.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment, { isDuration, Duration } from 'moment'; +import { isNumber } from 'lodash'; +const d = moment.duration; + +const roundingRules = [ + [d(500, 'ms'), d(100, 'ms')], + [d(5, 'second'), d(1, 'second')], + [d(7.5, 'second'), d(5, 'second')], + [d(15, 'second'), d(10, 'second')], + [d(45, 'second'), d(30, 'second')], + [d(3, 'minute'), d(1, 'minute')], + [d(9, 'minute'), d(5, 'minute')], + [d(20, 'minute'), d(10, 'minute')], + [d(45, 'minute'), d(30, 'minute')], + [d(2, 'hour'), d(1, 'hour')], + [d(6, 'hour'), d(3, 'hour')], + [d(24, 'hour'), d(12, 'hour')], + [d(1, 'week'), d(1, 'd')], + [d(3, 'week'), d(1, 'week')], + [d(1, 'year'), d(1, 'month')], + [Infinity, d(1, 'year')], +]; + +const revRoundingRules = [...roundingRules].reverse(); + +type NumberOrDuration = number | Duration; + +type Rule = NumberOrDuration[]; + +type CheckFunction = ( + bound: NumberOrDuration, + interval: Duration, + target: number +) => Duration | undefined; + +function findRule(rules: Rule[], check: CheckFunction, last?: boolean) { + function pickInterval(buckets: number, duration: Duration) { + const target = duration.asMilliseconds() / buckets; + let lastResult = null; + + for (const rule of rules) { + const result = check(rule[0] as Duration, rule[1] as Duration, target); + + if (result == null) { + if (!last) continue; + if (lastResult) return lastResult; + break; + } + + if (!last) return result; + lastResult = result; + } + + // fallback to just a number of milliseconds, ensure ms is >= 1 + const ms = Math.max(Math.floor(target), 1); + return moment.duration(ms, 'ms'); + } + + return (buckets: number, duration: Duration) => { + const interval = pickInterval(buckets, duration); + if (isDuration(interval)) return interval; + }; +} + +export const calculateAuto = { + near: findRule( + revRoundingRules, + function near(bound, interval, target) { + if (isDuration(bound) && bound.asMilliseconds() > target) return interval; + if (isNumber(bound) && bound > target) return interval; + }, + true + ), + + lessThan: findRule(revRoundingRules, function lessThan(_bound, interval, target) { + if (interval.asMilliseconds() < target) return interval; + }), + + atLeast: findRule(revRoundingRules, function atLeast(_bound, interval, target) { + if (interval.asMilliseconds() <= target) return interval; + }), +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_bucket_size.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_bucket_size.test.ts new file mode 100644 index 00000000000000..bc54593f10cb9f --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/calculate_bucket_size.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { calculateBucketSize } from './'; +import moment from 'moment'; + +const timerange = { + from: moment('2017-01-01T00:00:00.000Z').valueOf(), + to: moment('2017-01-01T01:00:00.000Z').valueOf(), + interval: '1m', + field: '@timetsamp', +}; + +describe('calculateBucketSize(timerange, intervalString)', () => { + test('returns auto calculated buckets', () => { + const result = calculateBucketSize({ ...timerange, interval: 'auto' }); + expect(result).toHaveProperty('bucketSize', 30); + expect(result).toHaveProperty('intervalString', '30s'); + }); + + test('returns overridden buckets (1s)', () => { + const result = calculateBucketSize({ ...timerange, interval: '1s' }); + expect(result).toHaveProperty('bucketSize', 1); + expect(result).toHaveProperty('intervalString', '1s'); + }); + + test('returns overridden buckets (10m)', () => { + const result = calculateBucketSize({ ...timerange, interval: '10m' }); + expect(result).toHaveProperty('bucketSize', 600); + expect(result).toHaveProperty('intervalString', '10m'); + }); + + test('returns overridden buckets (1d)', () => { + const result = calculateBucketSize({ ...timerange, interval: '1d' }); + expect(result).toHaveProperty('bucketSize', 86400); + expect(result).toHaveProperty('intervalString', '1d'); + }); + + test('returns overridden buckets (>=2d)', () => { + const result = calculateBucketSize({ ...timerange, interval: '>=2d' }); + expect(result).toHaveProperty('bucketSize', 86400 * 2); + expect(result).toHaveProperty('intervalString', '2d'); + }); + + test('returns overridden buckets (>=10s)', () => { + const result = calculateBucketSize({ ...timerange, interval: '>=10s' }); + expect(result).toHaveProperty('bucketSize', 30); + expect(result).toHaveProperty('intervalString', '30s'); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/index.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/index.ts new file mode 100644 index 00000000000000..62e4aed6d40494 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/index.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { MetricsAPITimerange } from '../../../../../common/http_api'; +import { calculateAuto } from './calculate_auto'; +import { + getUnitValue, + parseInterval, + convertIntervalToUnit, + ASCENDING_UNIT_ORDER, +} from './unit_to_seconds'; +import { INTERVAL_STRING_RE, GTE_INTERVAL_RE } from './interval_regex'; + +const calculateBucketData = (intervalString: string) => { + const intervalStringMatch = intervalString.match(INTERVAL_STRING_RE); + + if (!intervalStringMatch) { + throw new Error('Unable to parse interval string'); + } + + const parsedInterval = parseInterval(intervalString); + + if (!parsedInterval) { + throw new Error('Unable to parse interval string'); + } + + let bucketSize = Number(intervalStringMatch[1]) * getUnitValue(intervalStringMatch[2]); + + // don't go too small + if (bucketSize < 1) { + bucketSize = 1; + } + + // Check decimal + if (parsedInterval.value && parsedInterval.value % 1 !== 0) { + if (parsedInterval.unit && parsedInterval.unit !== 'ms') { + const { value, unit } = convertIntervalToUnit( + intervalString, + ASCENDING_UNIT_ORDER[ASCENDING_UNIT_ORDER.indexOf(parsedInterval.unit) - 1] + ); + + if (value && unit) { + intervalString = value + unit; + } else { + intervalString = '1ms'; + } + } else { + intervalString = '1ms'; + } + } + + return { + bucketSize, + intervalString, + }; +}; + +const calculateBucketSizeForAutoInterval = (timerange: MetricsAPITimerange): number | undefined => { + const duration = moment.duration(timerange.to - timerange.from, 'ms'); + const bucketSizeDuration = calculateAuto.near(100, duration); + if (bucketSizeDuration) { + return bucketSizeDuration.asSeconds(); + } +}; + +export const calculateBucketSize = (timerange: MetricsAPITimerange) => { + const bucketSize = calculateBucketSizeForAutoInterval(timerange); + let intervalString = `${bucketSize}s`; + + const gteAutoMatch = timerange.interval.match(GTE_INTERVAL_RE); + + if (gteAutoMatch) { + const bucketData = calculateBucketData(gteAutoMatch[1]); + if (bucketSize && bucketData.bucketSize >= bucketSize) { + return bucketData; + } + } + + const matches = timerange.interval.match(INTERVAL_STRING_RE); + if (matches) { + intervalString = timerange.interval; + } + + return calculateBucketData(intervalString); +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.test.ts new file mode 100644 index 00000000000000..6c2e9f8d62e36d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GTE_INTERVAL_RE, INTERVAL_STRING_RE } from './interval_regex'; + +describe('REGEX for Intervals', () => { + describe('GTE_INTERVAL_RE', () => { + test('returns true for">=12h"', () => { + const value = GTE_INTERVAL_RE.test('>=12h'); + + expect(value).toBeTruthy(); + }); + test('returns true for ">=1y"', () => { + const value = GTE_INTERVAL_RE.test('>=12h'); + + expect(value).toBeTruthy(); + }); + test('returns true for ">=25m"', () => { + const value = GTE_INTERVAL_RE.test('>=12h'); + + expect(value).toBeTruthy(); + }); + test('returns false "auto"', () => { + const value = GTE_INTERVAL_RE.test('auto'); + + expect(value).toBeFalsy(); + }); + test('returns false "wrongInput"', () => { + const value = GTE_INTERVAL_RE.test('wrongInput'); + + expect(value).toBeFalsy(); + }); + test('returns false "d"', () => { + const value = GTE_INTERVAL_RE.test('d'); + + expect(value).toBeFalsy(); + }); + + test('returns false "y"', () => { + const value = GTE_INTERVAL_RE.test('y'); + + expect(value).toBeFalsy(); + }); + }); + + describe('INTERVAL_STRING_RE', () => { + test('returns true for "8d"', () => { + const value = INTERVAL_STRING_RE.test('8d'); + + expect(value).toBeTruthy(); + }); + test('returns true for "1y"', () => { + const value = INTERVAL_STRING_RE.test('1y'); + + expect(value).toBeTruthy(); + }); + test('returns true for "6M"', () => { + const value = INTERVAL_STRING_RE.test('6M'); + + expect(value).toBeTruthy(); + }); + test('returns false "auto"', () => { + const value = INTERVAL_STRING_RE.test('auto'); + + expect(value).toBeFalsy(); + }); + test('returns false "wrongInput"', () => { + const value = INTERVAL_STRING_RE.test('wrongInput'); + + expect(value).toBeFalsy(); + }); + test('returns false for">=21h"', () => { + const value = INTERVAL_STRING_RE.test('>=21h'); + + expect(value).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.ts new file mode 100644 index 00000000000000..d86603551fea25 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/interval_regex.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; + +export const GTE_INTERVAL_RE = new RegExp(`^>=([\\d\\.]+\\s*(${dateMath.units.join('|')}))$`); +export const INTERVAL_STRING_RE = new RegExp(`^([\\d\\.]+)\\s*(${dateMath.units.join('|')})$`); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.test.ts new file mode 100644 index 00000000000000..68816f935a4a09 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getUnitValue, + parseInterval, + convertIntervalToUnit, + getSuitableUnit, +} from './unit_to_seconds'; + +describe('parseInterval()', () => { + test('should parse "1m" interval (positive)', () => + expect(parseInterval('1m')).toEqual({ + value: 1, + unit: 'm', + })); + + test('should parse "134d" interval (positive)', () => + expect(parseInterval('134d')).toEqual({ + value: 134, + unit: 'd', + })); + + test('should parse "0.5d" interval (positive)', () => + expect(parseInterval('0.5d')).toEqual({ + value: 0.5, + unit: 'd', + })); + + test('should parse "30M" interval (positive)', () => + expect(parseInterval('30M')).toEqual({ + value: 30, + unit: 'M', + })); + + test('should not parse "gm" interval (negative)', () => + expect(parseInterval('gm')).toEqual({ + value: undefined, + unit: undefined, + })); + + test('should not parse "-1d" interval (negative)', () => + expect(parseInterval('-1d')).toEqual({ + value: undefined, + unit: undefined, + })); + + test('should not parse "M" interval (negative)', () => + expect(parseInterval('M')).toEqual({ + value: undefined, + unit: undefined, + })); +}); + +describe('convertIntervalToUnit()', () => { + test('should convert "30m" interval to "h" unit (positive)', () => + expect(convertIntervalToUnit('30m', 'h')).toEqual({ + value: 0.5, + unit: 'h', + })); + + test('should convert "0.5h" interval to "m" unit (positive)', () => + expect(convertIntervalToUnit('0.5h', 'm')).toEqual({ + value: 30, + unit: 'm', + })); + + test('should convert "1h" interval to "m" unit (positive)', () => + expect(convertIntervalToUnit('1h', 'm')).toEqual({ + value: 60, + unit: 'm', + })); + + test('should convert "1h" interval to "ms" unit (positive)', () => + expect(convertIntervalToUnit('1h', 'ms')).toEqual({ + value: 3600000, + unit: 'ms', + })); + + test('should not convert "30m" interval to "0" unit (positive)', () => + expect(convertIntervalToUnit('30m', 'o')).toEqual({ + value: undefined, + unit: undefined, + })); + + test('should not convert "m" interval to "s" unit (positive)', () => + expect(convertIntervalToUnit('m', 's')).toEqual({ + value: undefined, + unit: undefined, + })); +}); + +describe('getSuitableUnit()', () => { + test('should return "d" unit for oneDayInSeconds (positive)', () => { + const oneDayInSeconds = getUnitValue('d') * 1; + + expect(getSuitableUnit(oneDayInSeconds)).toBe('d'); + }); + + test('should return "d" unit for twoDaysInSeconds (positive)', () => { + const twoDaysInSeconds = getUnitValue('d') * 2; + + expect(getSuitableUnit(twoDaysInSeconds)).toBe('d'); + }); + + test('should return "w" unit for threeWeeksInSeconds (positive)', () => { + const threeWeeksInSeconds = getUnitValue('w') * 3; + + expect(getSuitableUnit(threeWeeksInSeconds)).toBe('w'); + }); + + test('should return "y" unit for aroundOneYearInSeconds (positive)', () => { + const aroundOneYearInSeconds = getUnitValue('d') * 370; + + expect(getSuitableUnit(aroundOneYearInSeconds)).toBe('y'); + }); + + test('should return "y" unit for twoYearsInSeconds (positive)', () => { + const twoYearsInSeconds = getUnitValue('y') * 2; + + expect(getSuitableUnit(twoYearsInSeconds)).toBe('y'); + }); + + test('should return "undefined" unit for negativeNumber (negative)', () => { + const negativeNumber = -12; + + expect(getSuitableUnit(negativeNumber)).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.ts new file mode 100644 index 00000000000000..7ca4222fe352fd --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_bucket_size/unit_to_seconds.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy, isNumber } from 'lodash'; +import { INTERVAL_STRING_RE } from './interval_regex'; + +export const ASCENDING_UNIT_ORDER = ['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y']; + +const units: Record = { + ms: 0.001, + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 86400 * 7, + M: 86400 * 30, + y: 86400 * 365, +}; + +const sortedUnits = sortBy(Object.keys(units), (key) => units[key]); + +export const parseInterval = (intervalString: string) => { + let value; + let unit; + + if (intervalString) { + const matches = intervalString.match(INTERVAL_STRING_RE); + + if (matches) { + value = Number(matches[1]); + unit = matches[2]; + } + } + + return { value, unit }; +}; + +export const convertIntervalToUnit = (intervalString: string, newUnit: string) => { + const parsedInterval = parseInterval(intervalString); + let value; + let unit; + + if (parsedInterval.unit && parsedInterval.value && units[newUnit]) { + value = Number( + ((parsedInterval.value * units[parsedInterval.unit]) / units[newUnit]).toFixed(2) + ); + unit = newUnit; + } + + return { value, unit }; +}; + +export const getSuitableUnit = (intervalInSeconds: number) => + sortedUnits.find((key, index, array) => { + const nextUnit = array[index + 1]; + const isValidInput = isNumber(intervalInSeconds) && intervalInSeconds > 0; + const isLastItem = index + 1 === array.length; + + return ( + isValidInput && + ((intervalInSeconds >= units[key] && intervalInSeconds < units[nextUnit]) || isLastItem) + ); + }); + +export const getUnitValue = (unit: string) => units[unit]; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.test.ts new file mode 100644 index 00000000000000..4fa313eea18521 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { calculateDateHistogramOffset } from './calculate_date_histogram_offset'; +import moment from 'moment'; + +describe('calculateDateHistogramOffset(timerange)', () => { + it('should just work', () => { + const timerange = { + from: moment('2020-01-01T00:03:32').valueOf(), + to: moment('2020-01-01T01:03:32').valueOf(), + interval: '1m', + field: '@timestamp', + }; + const offset = calculateDateHistogramOffset(timerange); + expect(offset).toBe('-28s'); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.ts new file mode 100644 index 00000000000000..673747808f5ca4 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_date_histogram_offset.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPITimerange } from '../../../../common/http_api'; +import { calculateBucketSize } from './calculate_bucket_size'; + +export const calculateDateHistogramOffset = (timerange: MetricsAPITimerange): string => { + const fromInSeconds = Math.floor(timerange.from / 1000); + const { bucketSize } = calculateBucketSize(timerange); + + // negative offset to align buckets with full intervals (e.g. minutes) + const offset = (fromInSeconds % bucketSize) - bucketSize; + return `${offset}s`; +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts new file mode 100644 index 00000000000000..d9de8ae7e61e5d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPIRequest } from '../../../../common/http_api'; +import moment from 'moment'; +import { convertHistogramBucketsToTimeseries } from './convert_histogram_buckets_to_timeseries'; + +const keys = ['example-0']; + +const options: MetricsAPIRequest = { + timerange: { + field: '@timestamp', + from: moment('2020-01-01T00:00:00Z').valueOf(), + to: moment('2020-01-01T01:00:00Z').valueOf(), + interval: '1m', + }, + limit: 9, + indexPattern: 'metrics-*', + metrics: [ + { id: 'metric_0', aggregations: { metric_0: { avg: { field: 'system.cpu.user.pct' } } } }, + ], +}; + +const buckets = [ + { + key: moment('2020-01-01T00:00:00Z').valueOf(), + key_as_string: moment('2020-01-01T00:00:00Z').toISOString(), + doc_count: 1, + metric_0: { value: 1 }, + }, + { + key: moment('2020-01-01T00:00:00Z').add(1, 'minute').valueOf(), + key_as_string: moment('2020-01-01T00:00:00Z').add(1, 'minute').toISOString(), + doc_count: 1, + metric_0: { value: 1 }, + }, + { + key: moment('2020-01-01T00:00:00Z').add(2, 'minute').valueOf(), + key_as_string: moment('2020-01-01T00:00:00Z').add(2, 'minute').toISOString(), + doc_count: 1, + metric_0: { value: 1 }, + }, + { + key: moment('2020-01-01T00:00:00Z').add(2, 'minute').valueOf(), + key_as_string: moment('2020-01-01T00:00:00Z').add(2, 'minute').toISOString(), + doc_count: 1, + metric_0: { value: null }, + }, +]; + +describe('convertHistogramBucketsToTimeseies(keys, options, buckets)', () => { + it('should just work', () => { + expect(convertHistogramBucketsToTimeseries(keys, options, buckets)).toMatchSnapshot(); + }); + it('should drop the last bucket', () => { + expect( + convertHistogramBucketsToTimeseries(keys, { ...options, dropLastBucket: true }, buckets) + ).toMatchSnapshot(); + }); + it('should return empty timeseries for empty metrics', () => { + expect( + convertHistogramBucketsToTimeseries(keys, { ...options, metrics: [] }, buckets) + ).toMatchSnapshot(); + }); + it('should work with normalized_values', () => { + const bucketsWithNormalizedValue = buckets.map((bucket) => { + const value = bucket.metric_0.value; + if (value) { + return { ...bucket, metric_0: { value, normalized_value: value + 1 } }; + } + return bucket; + }); + expect( + convertHistogramBucketsToTimeseries(keys, { ...options }, bucketsWithNormalizedValue) + ).toMatchSnapshot(); + }); + it('should work with percentiles', () => { + const bucketsWithPercentiles = buckets.map((bucket) => { + return { ...bucket, metric_0: { values: { '95.0': 3 } } }; + }); + expect( + convertHistogramBucketsToTimeseries(keys, { ...options }, bucketsWithPercentiles) + ).toMatchSnapshot(); + }); + it('should throw error with multiple percentiles', () => { + const bucketsWithMultiplePercentiles = buckets.map((bucket) => { + return { ...bucket, metric_0: { values: { '95.0': 3, '99.0': 4 } } }; + }); + expect(() => + convertHistogramBucketsToTimeseries(keys, { ...options }, bucketsWithMultiplePercentiles) + ).toThrow(); + }); + it('should work with keyed percentiles', () => { + const bucketsWithKeyedPercentiles = buckets.map((bucket) => { + return { ...bucket, metric_0: { values: [{ key: '99.0', value: 4 }] } }; + }); + expect( + convertHistogramBucketsToTimeseries(keys, { ...options }, bucketsWithKeyedPercentiles) + ).toMatchSnapshot(); + }); + it('should throw error with multiple keyed percentiles', () => { + const bucketsWithMultipleKeyedPercentiles = buckets.map((bucket) => { + return { + ...bucket, + metric_0: { + values: [ + { key: '95.0', value: 3 }, + { key: '99.0', value: 4 }, + ], + }, + }; + }); + expect(() => + convertHistogramBucketsToTimeseries(keys, { ...options }, bucketsWithMultipleKeyedPercentiles) + ).toThrow(); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts new file mode 100644 index 00000000000000..95e6ece2151339 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, values, first } from 'lodash'; +import { + MetricsAPIRequest, + MetricsAPISeries, + MetricsAPIColumn, + MetricsAPIRow, +} from '../../../../common/http_api/metrics_api'; +import { + HistogramBucket, + MetricValueType, + BasicMetricValueRT, + NormalizedMetricValueRT, + PercentilesTypeRT, + PercentilesKeyedTypeRT, +} from '../types'; +const BASE_COLUMNS = [{ name: 'timestamp', type: 'date' }] as MetricsAPIColumn[]; + +const getValue = (valueObject: string | number | MetricValueType) => { + if (NormalizedMetricValueRT.is(valueObject)) { + return valueObject.normalized_value || valueObject.value; + } + + if (PercentilesTypeRT.is(valueObject)) { + const percentileValues = values(valueObject.values); + if (percentileValues.length > 1) { + throw new Error( + 'Metrics API only supports a single percentile, multiple percentiles should be sent separately' + ); + } + return first(percentileValues) || null; + } + + if (PercentilesKeyedTypeRT.is(valueObject)) { + if (valueObject.values.length > 1) { + throw new Error( + 'Metrics API only supports a single percentile, multiple percentiles should be sent separately' + ); + } + const percentileValue = first(valueObject.values); + return (percentileValue && percentileValue.value) || null; + } + + if (BasicMetricValueRT.is(valueObject)) { + return valueObject.value; + } + + return null; +}; + +const convertBucketsToRows = ( + options: MetricsAPIRequest, + buckets: HistogramBucket[] +): MetricsAPIRow[] => { + return buckets.map((bucket) => { + const ids = options.metrics.map((metric) => metric.id); + const metrics = ids.reduce((acc, id) => { + const valueObject = get(bucket, [id]); + return { ...acc, [id]: getValue(valueObject) }; + }, {} as Record); + return { timestamp: bucket.key as number, ...metrics }; + }); +}; + +export const convertHistogramBucketsToTimeseries = ( + keys: string[], + options: MetricsAPIRequest, + buckets: HistogramBucket[] +): MetricsAPISeries => { + const id = keys.join(':'); + // If there are no metrics then we just return the empty series + // but still maintain the groupings. + if (options.metrics.length === 0) { + return { id, keys, columns: [], rows: [] }; + } + const columns = options.metrics.map((metric) => ({ + name: metric.id, + type: 'number', + })) as MetricsAPIColumn[]; + const allRows = convertBucketsToRows(options, buckets); + const rows = options.dropLastBucket ? allRows.slice(0, allRows.length - 1) : allRows; + return { + id, + keys, + rows, + columns: [...BASE_COLUMNS, ...columns], + }; +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts new file mode 100644 index 00000000000000..1353351cde8a22 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAggregations } from './create_aggregations'; +import moment from 'moment'; +import { MetricsAPIRequest } from '../../../../common/http_api'; + +const options: MetricsAPIRequest = { + timerange: { + field: '@timestamp', + from: moment('2020-01-01T00:00:00Z').valueOf(), + to: moment('2020-01-01T01:00:00Z').valueOf(), + interval: '>=1m', + }, + limit: 20, + indexPattern: 'metrics-*', + metrics: [ + { id: 'metric_0', aggregations: { metric_0: { avg: { field: 'system.cpu.user.pct' } } } }, + ], +}; + +describe('createAggregations(options)', () => { + it('should return groupings aggregation with groupBy', () => { + const optionsWithGroupBy = { ...options, groupBy: ['host.name'] }; + expect(createAggregations(optionsWithGroupBy)).toMatchSnapshot(); + }); + it('should return just histogram aggregation without groupBy', () => { + expect(createAggregations(options)).toMatchSnapshot(); + }); + it('should return add offset to histogram', () => { + const optionsWithAlignDataToEnd = { + ...options, + timerange: { + ...options.timerange, + from: moment('2020-01-01T00:00:00Z').subtract(28, 'minutes').valueOf(), + to: moment('2020-01-01T01:00:00Z').subtract(28, 'minutes').valueOf(), + }, + alignDataToEnd: true, + }; + expect(createAggregations(optionsWithAlignDataToEnd)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts new file mode 100644 index 00000000000000..991e5febfc6345 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPIRequest } from '../../../../common/http_api/metrics_api'; +import { calculateDateHistogramOffset } from './calculate_date_histogram_offset'; +import { createMetricsAggregations } from './create_metrics_aggregations'; +import { calculateBucketSize } from './calculate_bucket_size'; + +export const createAggregations = (options: MetricsAPIRequest) => { + const { intervalString } = calculateBucketSize(options.timerange); + const histogramAggregation = { + histogram: { + date_histogram: { + field: options.timerange.field, + fixed_interval: intervalString, + offset: options.alignDataToEnd ? calculateDateHistogramOffset(options.timerange) : '0s', + extended_bounds: { + min: options.timerange.from, + max: options.timerange.to, + }, + }, + aggregations: createMetricsAggregations(options), + }, + }; + + if (Array.isArray(options.groupBy) && options.groupBy.length) { + const limit = options.limit || 9; + return { + groupings: { + composite: { + size: limit, + sources: options.groupBy.map((field, index) => ({ + [`groupBy${index}`]: { terms: { field, order: 'asc' } }, + })), + }, + aggs: histogramAggregation, + }, + }; + } + + return histogramAggregation; +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts new file mode 100644 index 00000000000000..a18b8fd533c7fc --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPIRequest } from '../../../../common/http_api'; +import moment from 'moment'; +import { createMetricsAggregations } from './create_metrics_aggregations'; + +const options: MetricsAPIRequest = { + timerange: { + field: '@timestamp', + from: moment('2020-01-01T00:00:00Z').valueOf(), + to: moment('2020-01-01T01:00:00Z').valueOf(), + interval: '>=1m', + }, + limit: 20, + indexPattern: 'metrics-*', + metrics: [ + { id: 'metric_0', aggregations: { metric_0: { avg: { field: 'system.cpu.user.pct' } } } }, + { + id: 'metric_1', + aggregations: { + metric_1_max: { max: { field: 'system.network.in.bytes' } }, + metric_1: { derivative: { buckets_path: 'metric_1_max', gap_policy: 'skip', unit: '1s' } }, + }, + }, + ], +}; + +describe('createMetricsAggregations(options)', () => { + it('should just work', () => { + expect(createMetricsAggregations(options)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.ts new file mode 100644 index 00000000000000..c1400ea4078299 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsUIAggregation } from '../../../../common/inventory_models/types'; +import { MetricsAPIRequest } from '../../../../common/http_api/metrics_api'; + +export const createMetricsAggregations = (options: MetricsAPIRequest): MetricsUIAggregation => { + const { metrics } = options; + return metrics.reduce((aggs, metric) => { + return { ...aggs, ...metric.aggregations }; + }, {}); +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/types.ts b/x-pack/plugins/infra/server/lib/metrics/types.ts new file mode 100644 index 00000000000000..d1866470e0cf9d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/types.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework'; + +export type ESSearchClient = ( + options: CallWithRequestParams +) => Promise>; + +const NumberOrNullRT = rt.union([rt.number, rt.null]); + +export const BasicMetricValueRT = rt.type({ value: NumberOrNullRT }); + +export const NormalizedMetricValueRT = rt.intersection([ + BasicMetricValueRT, + rt.type({ normalized_value: NumberOrNullRT }), +]); +export const PercentilesTypeRT = rt.type({ values: rt.record(rt.string, NumberOrNullRT) }); + +export const PercentilesKeyedTypeRT = rt.type({ + values: rt.array(rt.type({ key: rt.string, value: NumberOrNullRT })), +}); + +export const MetricValueTypeRT = rt.union([ + BasicMetricValueRT, + NormalizedMetricValueRT, + PercentilesTypeRT, + PercentilesKeyedTypeRT, +]); +export type MetricValueType = rt.TypeOf; + +export const HistogramBucketRT = rt.record( + rt.string, + rt.union([rt.number, rt.string, MetricValueTypeRT]) +); + +export const HistogramResponseRT = rt.type({ + histogram: rt.type({ + buckets: rt.array(HistogramBucketRT), + }), +}); + +const GroupingBucketRT = rt.intersection([ + rt.type({ + key: rt.record(rt.string, rt.string), + doc_count: rt.number, + }), + HistogramResponseRT, +]); + +export const GroupingResponseRT = rt.type({ + groupings: rt.intersection([ + rt.type({ + buckets: rt.array(GroupingBucketRT), + }), + rt.partial({ + after_key: rt.record(rt.string, rt.string), + }), + ]), +}); + +export type HistogramBucket = rt.TypeOf; + +export type HistogramResponse = rt.TypeOf; + +export type GroupingResponse = rt.TypeOf; + +export type MetricsESResponse = HistogramResponse | GroupingResponse; diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index d1a4ed431a2be0..719ffdb8fa7c40 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -8,7 +8,7 @@ import { uniq } from 'lodash'; import { InfraSnapshotRequestOptions } from './types'; import { getMetricsAggregations } from './query_helpers'; import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; -import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types'; +import { MetricsUIAggregation, ESBasicMetricAggRT } from '../../../common/inventory_models/types'; import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field'; import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; import { ESSearchClient } from '.'; @@ -59,12 +59,12 @@ export const createTimeRangeWithInterval = async ( const aggregationsToModules = async ( client: ESSearchClient, - aggregations: SnapshotModel, + aggregations: MetricsUIAggregation, options: InfraSnapshotRequestOptions ): Promise => { const uniqueFields = Object.values(aggregations) .reduce>((fields, agg) => { - if (SnapshotModelMetricAggRT.is(agg)) { + if (ESBasicMetricAggRT.is(agg)) { return uniq(fields.concat(Object.values(agg).map((a) => a?.field))); } return fields; diff --git a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts index 732fa10decc986..ca63043ba868e2 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts @@ -9,8 +9,8 @@ import { findInventoryModel, findInventoryFields } from '../../../common/invento import { InfraSnapshotRequestOptions } from './types'; import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; import { - SnapshotModelRT, - SnapshotModel, + MetricsUIAggregation, + MetricsUIAggregationRT, InventoryItemType, } from '../../../common/inventory_models/types'; import { @@ -75,11 +75,13 @@ export const metricToAggregation = ( return inventoryModel.metrics.snapshot?.[metric.type]; }; -export const getMetricsAggregations = (options: InfraSnapshotRequestOptions): SnapshotModel => { +export const getMetricsAggregations = ( + options: InfraSnapshotRequestOptions +): MetricsUIAggregation => { const { metrics } = options; return metrics.reduce((aggs, metric, index) => { const aggregation = metricToAggregation(options.nodeType, metric, index); - if (!SnapshotModelRT.is(aggregation)) { + if (!MetricsUIAggregationRT.is(aggregation)) { throw new Error( i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', { defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.', diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index b096bed84fa9a8..82b2852099bae8 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -4,20 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + METRICS_INDEX_PATTERN, + LOGS_INDEX_PATTERN, + TIMESTAMP_FIELD, +} from '../../../common/constants'; import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; export const defaultSourceConfiguration: InfraSourceConfiguration = { name: 'Default', description: '', - metricAlias: 'metrics-*,metricbeat-*', - logAlias: 'logs-*,filebeat-*,kibana_sample_data_logs*', + metricAlias: METRICS_INDEX_PATTERN, + logAlias: LOGS_INDEX_PATTERN, fields: { container: 'container.id', host: 'host.name', message: ['message', '@message'], pod: 'kubernetes.pod.uid', tiebreaker: '_doc', - timestamp: '@timestamp', + timestamp: TIMESTAMP_FIELD, }, inventoryDefaultView: '0', metricsExplorerDefaultView: '0', diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts index c22095a31195ab..4b90b52cb51f67 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts @@ -10,17 +10,23 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../lib/infra_types'; -import { getGroupings } from './lib/get_groupings'; -import { populateSeriesWithTSVBData } from './lib/populate_series_with_tsvb_data'; -import { metricsExplorerRequestBodyRT, metricsExplorerResponseRT } from '../../../common/http_api'; +import { + metricsExplorerRequestBodyRT, + metricsExplorerResponseRT, + MetricsExplorerPageInfo, +} from '../../../common/http_api'; import { throwErrors } from '../../../common/runtime_types'; +import { convertRequestToMetricsAPIOptions } from './lib/convert_request_to_metrics_api_options'; +import { createSearchClient } from '../../lib/create_search_client'; +import { findIntervalForMetrics } from './lib/find_interval_for_metrics'; +import { query } from '../../lib/metrics'; +import { queryTotalGroupings } from './lib/query_total_groupings'; +import { transformSeries } from './lib/transform_series'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - const { callWithRequest } = framework; - framework.registerRoute( { method: 'post', @@ -31,26 +37,48 @@ export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { }, async (requestContext, request, response) => { try { - const payload = pipe( + const options = pipe( metricsExplorerRequestBodyRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const search = (searchOptions: object) => - callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); + const client = createSearchClient(requestContext, framework); + const interval = await findIntervalForMetrics(client, options); - // First we get the groupings from a composite aggregation - const groupings = await getGroupings(search, payload); + const optionsWithInterval = options.forceInterval + ? options + : { + ...options, + timerange: { + ...options.timerange, + interval: interval ? `>=${interval}s` : options.timerange.interval, + }, + }; + + const metricsApiOptions = convertRequestToMetricsAPIOptions(optionsWithInterval); + const metricsApiResponse = await query(client, metricsApiOptions); + const totalGroupings = await queryTotalGroupings(client, metricsApiOptions); + const hasGroupBy = + Array.isArray(metricsApiOptions.groupBy) && metricsApiOptions.groupBy.length > 0; + + const pageInfo: MetricsExplorerPageInfo = { + total: totalGroupings, + afterKey: null, + }; + + if (metricsApiResponse.info.afterKey) { + pageInfo.afterKey = metricsApiResponse.info.afterKey; + } + + // If we have a groupBy but there are ZERO groupings returned then we need to + // return an empty array. Otherwise we transform the series to match the current schema. + const series = + hasGroupBy && totalGroupings === 0 + ? [] + : metricsApiResponse.series.map(transformSeries(hasGroupBy)); - // Then we take the results and fill in the data from TSVB with the - // user's custom metrics - const seriesWithMetrics = await Promise.all( - groupings.series.map( - populateSeriesWithTSVBData(request, payload, framework, requestContext) - ) - ); return response.ok({ - body: metricsExplorerResponseRT.encode({ ...groupings, series: seriesWithMetrics }), + body: metricsExplorerResponseRT.encode({ series, pageInfo }), }); } catch (error) { return response.internalError({ diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.test.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.test.ts new file mode 100644 index 00000000000000..67dbaf8e1e8777 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { convertMetricToMetricsAPIMetric } from './convert_metric_to_metrics_api_metric'; +import { + MetricsExplorerMetric, + MetricsAPIMetric, + MetricsExplorerAggregation, +} from '../../../../common/http_api'; + +describe('convertMetricToMetricsAPIMetric(metric, index)', () => { + const runTest = (metric: MetricsExplorerMetric, aggregation: MetricsAPIMetric) => + it(`should convert ${metric.aggregation}`, () => { + expect(convertMetricToMetricsAPIMetric(metric, 1)).toEqual(aggregation); + }); + + const runTestForBasic = (aggregation: MetricsExplorerAggregation) => + runTest( + { aggregation, field: 'system.cpu.user.pct' }, + { + id: 'metric_1', + aggregations: { metric_1: { [aggregation]: { field: 'system.cpu.user.pct' } } }, + } + ); + + runTestForBasic('avg'); + runTestForBasic('sum'); + runTestForBasic('max'); + runTestForBasic('min'); + runTestForBasic('cardinality'); + + runTest( + { aggregation: 'rate', field: 'system.network.in.bytes' }, + { + id: 'metric_1', + aggregations: { + metric_1_max: { + max: { + field: 'system.network.in.bytes', + }, + }, + metric_1_deriv: { + derivative: { + buckets_path: 'metric_1_max', + gap_policy: 'skip', + unit: '1s', + }, + }, + metric_1: { + bucket_script: { + buckets_path: { + value: 'metric_1_deriv[normalized_value]', + }, + gap_policy: 'skip', + script: { + lang: 'painless', + source: 'params.value > 0.0 ? params.value : 0.0', + }, + }, + }, + }, + } + ); + + runTest( + { aggregation: 'count' }, + { + id: 'metric_1', + aggregations: { + metric_1: { + bucket_script: { + buckets_path: { + count: '_count', + }, + gap_policy: 'skip', + script: { + lang: 'expression', + source: 'count * 1', + }, + }, + }, + }, + } + ); +}); diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts new file mode 100644 index 00000000000000..93948a8b8797e9 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; +import { MetricsAPIMetric, MetricsExplorerMetric } from '../../../../common/http_api'; + +export const convertMetricToMetricsAPIMetric = ( + metric: MetricsExplorerMetric, + index: number +): MetricsAPIMetric => { + const id = `metric_${index}`; + if (metric.aggregation === 'rate' && metric.field) { + return { + id, + aggregations: networkTraffic(id, metric.field), + }; + } + + if (['p95', 'p99'].includes(metric.aggregation) && metric.field) { + const percent = metric.aggregation === 'p95' ? 95 : 99; + return { + id, + aggregations: { + [id]: { + percentiles: { + field: metric.field, + percents: [percent], + }, + }, + }, + }; + } + + if (['max', 'min', 'avg', 'cardinality', 'sum'].includes(metric.aggregation) && metric.field) { + return { + id, + aggregations: { + [id]: { + [metric.aggregation]: { field: metric.field }, + }, + }, + }; + } + + return { + id, + aggregations: { + [id]: { + bucket_script: { + buckets_path: { count: '_count' }, + script: { + source: 'count * 1', + lang: 'expression', + }, + gap_policy: 'skip', + }, + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts new file mode 100644 index 00000000000000..4c423aee347e9e --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsExplorerRequestBody, MetricsAPIRequest } from '../../../../common/http_api'; +import { convertRequestToMetricsAPIOptions } from './convert_request_to_metrics_api_options'; + +const BASE_REQUEST: MetricsExplorerRequestBody = { + timerange: { + field: '@timestamp', + from: new Date('2020-01-01T00:00:00Z').getTime(), + to: new Date('2020-01-01T01:00:00Z').getTime(), + interval: '1m', + }, + limit: 9, + indexPattern: 'metrics-*', + metrics: [{ aggregation: 'avg', field: 'system.cpu.user.pct' }], +}; + +const BASE_METRICS_UI_OPTIONS: MetricsAPIRequest = { + timerange: { + field: '@timestamp', + from: new Date('2020-01-01T00:00:00Z').getTime(), + to: new Date('2020-01-01T01:00:00Z').getTime(), + interval: '1m', + }, + limit: 9, + dropLastBucket: true, + indexPattern: 'metrics-*', + metrics: [ + { id: 'metric_0', aggregations: { metric_0: { avg: { field: 'system.cpu.user.pct' } } } }, + ], +}; + +describe('convertRequestToMetricsAPIOptions', () => { + it('should just work', () => { + expect(convertRequestToMetricsAPIOptions(BASE_REQUEST)).toEqual(BASE_METRICS_UI_OPTIONS); + }); + + it('should work with string afterKeys', () => { + expect(convertRequestToMetricsAPIOptions({ ...BASE_REQUEST, afterKey: 'host.name' })).toEqual({ + ...BASE_METRICS_UI_OPTIONS, + afterKey: { groupBy0: 'host.name' }, + }); + }); + + it('should work with afterKey objects', () => { + const afterKey = { groupBy0: 'host.name', groupBy1: 'cloud.availability_zone' }; + expect( + convertRequestToMetricsAPIOptions({ + ...BASE_REQUEST, + afterKey, + }) + ).toEqual({ + ...BASE_METRICS_UI_OPTIONS, + afterKey, + }); + }); + + it('should work with string group bys', () => { + expect( + convertRequestToMetricsAPIOptions({ + ...BASE_REQUEST, + groupBy: 'host.name', + }) + ).toEqual({ + ...BASE_METRICS_UI_OPTIONS, + groupBy: ['host.name'], + }); + }); + + it('should work with group by arrays', () => { + expect( + convertRequestToMetricsAPIOptions({ + ...BASE_REQUEST, + groupBy: ['host.name', 'cloud.availability_zone'], + }) + ).toEqual({ + ...BASE_METRICS_UI_OPTIONS, + groupBy: ['host.name', 'cloud.availability_zone'], + }); + }); + + it('should work with filterQuery json string', () => { + const filter = { bool: { filter: [{ match: { 'host.name': 'example-01' } }] } }; + expect( + convertRequestToMetricsAPIOptions({ + ...BASE_REQUEST, + filterQuery: JSON.stringify(filter), + }) + ).toEqual({ + ...BASE_METRICS_UI_OPTIONS, + filters: [filter], + }); + }); + + it('should work with filterQuery as Lucene expressions', () => { + const filter = `host.name: 'example-01'`; + expect( + convertRequestToMetricsAPIOptions({ + ...BASE_REQUEST, + filterQuery: filter, + }) + ).toEqual({ + ...BASE_METRICS_UI_OPTIONS, + filters: [{ query_string: { query: filter, analyze_wildcard: true } }], + }); + }); + + it('should work with empty metrics', () => { + expect( + convertRequestToMetricsAPIOptions({ + ...BASE_REQUEST, + metrics: [], + }) + ).toEqual({ + ...BASE_METRICS_UI_OPTIONS, + metrics: [], + }); + }); +}); diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.ts new file mode 100644 index 00000000000000..2dd00c4aed59cc --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isObject, isArray } from 'lodash'; +import { + MetricsAPIRequest, + MetricsExplorerRequestBody, + afterKeyObjectRT, +} from '../../../../common/http_api'; +import { convertMetricToMetricsAPIMetric } from './convert_metric_to_metrics_api_metric'; + +export const convertRequestToMetricsAPIOptions = ( + options: MetricsExplorerRequestBody +): MetricsAPIRequest => { + const metrics = options.metrics.map(convertMetricToMetricsAPIMetric); + const { limit, timerange, indexPattern } = options; + + const metricsApiOptions: MetricsAPIRequest = { + timerange, + indexPattern, + limit, + metrics, + dropLastBucket: true, + }; + + if (options.afterKey) { + metricsApiOptions.afterKey = afterKeyObjectRT.is(options.afterKey) + ? options.afterKey + : { groupBy0: options.afterKey }; + } + + if (options.groupBy) { + metricsApiOptions.groupBy = isArray(options.groupBy) ? options.groupBy : [options.groupBy]; + } + + if (options.filterQuery) { + try { + const filterObject = JSON.parse(options.filterQuery); + if (isObject(filterObject)) { + metricsApiOptions.filters = [filterObject]; + } + } catch (err) { + metricsApiOptions.filters = [ + { + query_string: { + query: options.filterQuery, + analyze_wildcard: true, + }, + }, + ]; + } + } + + return metricsApiOptions; +}; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts deleted file mode 100644 index 3a9abf525a9f08..00000000000000 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { MetricsExplorerRequestBody } from '../../../../common/http_api/metrics_explorer'; -import { TSVBMetricModel } from '../../../../common/inventory_models/types'; - -const percentileToVaue = (agg: 'p95' | 'p99') => { - if (agg === 'p95') { - return 95; - } - return 99; -}; - -export const createMetricModel = (options: MetricsExplorerRequestBody): TSVBMetricModel => { - // if dropLastBucket is set use the value otherwise default to true. - const dropLastBucket: boolean = options.dropLastBucket != null ? options.dropLastBucket : true; - return { - id: 'custom', - requires: [], - index_pattern: options.indexPattern, - interval: options.timerange.interval, - time_field: options.timerange.field, - drop_last_bucket: dropLastBucket, - type: 'timeseries', - // Create one series per metric requested. The series.id will be used to identify the metric - // when the responses are processed and combined with the grouping request. - series: options.metrics.map((metric, index) => { - // If the metric is a rate then we need to add TSVB metrics for calculating the derivative - if (metric.aggregation === 'rate') { - const aggType = 'max'; - return { - id: `metric_${index}`, - split_mode: 'everything', - metrics: [ - { - id: `metric_${aggType}_${index}`, - field: metric.field, - type: aggType, - }, - { - id: `metric_deriv_${aggType}_${index}`, - field: `metric_${aggType}_${index}`, - type: 'derivative', - unit: '1s', - }, - { - id: `metric_posonly_deriv_${aggType}_${index}`, - type: 'calculation', - variables: [ - { id: 'var-rate', name: 'rate', field: `metric_deriv_${aggType}_${index}` }, - ], - script: 'params.rate > 0.0 ? params.rate : 0.0', - }, - ], - }; - } - - if (metric.aggregation === 'p95' || metric.aggregation === 'p99') { - return { - id: `metric_${index}`, - split_mode: 'everything', - metrics: [ - { - field: metric.field, - id: `metric_${metric.aggregation}_${index}`, - type: 'percentile', - percentiles: [ - { - id: 'percentile_0', - value: percentileToVaue(metric.aggregation), - }, - ], - }, - ], - }; - } - - // Create a basic TSVB series with a single metric - const aggregation = metric.aggregation || 'avg'; - - return { - id: `metric_${index}`, - split_mode: 'everything', - metrics: [ - { - field: metric.field, - id: `metric_${aggregation}_${index}`, - type: aggregation, - }, - ], - }; - }), - }; -}; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts new file mode 100644 index 00000000000000..876bbb41994416 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uniq } from 'lodash'; +import LRU from 'lru-cache'; +import { MetricsExplorerRequestBody } from '../../../../common/http_api'; +import { ESSearchClient } from '../../../lib/snapshot'; +import { getDatasetForField } from './get_dataset_for_field'; +import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; + +const cache = new LRU({ + max: 100, + maxAge: 15 * 60 * 1000, +}); + +export const findIntervalForMetrics = async ( + client: ESSearchClient, + options: MetricsExplorerRequestBody +) => { + const fields = uniq( + options.metrics.map((metric) => (metric.field ? metric.field : null)).filter((f) => f) + ) as string[]; + + const cacheKey = fields.sort().join(':'); + + if (cache.has(cacheKey)) return cache.get(cacheKey); + + if (fields.length === 0) { + return 60; + } + + const modules = await Promise.all( + fields.map( + async (field) => await getDatasetForField(client, field as string, options.indexPattern) + ) + ); + + const interval = calculateMetricInterval( + client, + { + indexPattern: options.indexPattern, + timestampField: options.timerange.field, + timerange: options.timerange, + }, + modules.filter(Boolean) as string[] + ); + cache.set(cacheKey, interval); + return interval; +}; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts deleted file mode 100644 index fdecb5f3d93153..00000000000000 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { set } from '@elastic/safer-lodash-set'; -import { isObject } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework'; -import { - MetricsExplorerRequestBody, - MetricsExplorerResponse, - afterKeyObjectRT, -} from '../../../../common/http_api/metrics_explorer'; - -interface GroupingAggregation { - groupingsCount: { - value: number; - }; - groupings: { - after_key?: { - [name: string]: string; - }; - buckets: Array<{ key: { [id: string]: string }; doc_count: number }>; - }; -} - -const EMPTY_RESPONSE = { - series: [ - { - id: i18n.translate('xpack.infra.metricsExploer.everything', { defaultMessage: 'Everything' }), - columns: [], - rows: [], - }, - ], - pageInfo: { total: 0, afterKey: null }, -}; - -export const getGroupings = async ( - search: (options: object) => Promise>, - options: MetricsExplorerRequestBody -): Promise => { - if (!options.groupBy) { - return EMPTY_RESPONSE; - } - - if (Array.isArray(options.groupBy) && options.groupBy.length === 0) { - return EMPTY_RESPONSE; - } - - const limit = options.limit || 9; - const groupBy = Array.isArray(options.groupBy) ? options.groupBy : [options.groupBy]; - const filter: Array> = [ - { - range: { - [options.timerange.field]: { - gte: options.timerange.from, - lte: options.timerange.to, - format: 'epoch_millis', - }, - }, - }, - ...groupBy.map((field) => ({ exists: { field } })), - ]; - const params = { - allowNoIndices: true, - ignoreUnavailable: true, - index: options.indexPattern, - body: { - size: 0, - query: { - bool: { - should: [ - ...options.metrics - .filter((m) => m.field) - .map((m) => ({ - exists: { field: m.field }, - })), - ], - filter, - }, - }, - aggs: { - groupingsCount: { - cardinality: { - script: { source: groupBy.map((field) => `doc['${field}'].value`).join('+') }, - }, - }, - groupings: { - composite: { - size: limit, - sources: groupBy.map((field, index) => ({ - [`groupBy${index}`]: { terms: { field, order: 'asc' } }, - })), - }, - }, - }, - }, - }; - - if (params.body.query.bool.should.length !== 0) { - set(params, 'body.query.bool.minimum_should_match', 1); - } - - if (options.afterKey) { - if (afterKeyObjectRT.is(options.afterKey)) { - set(params, 'body.aggs.groupings.composite.after', options.afterKey); - } else { - set(params, 'body.aggs.groupings.composite.after', { groupBy0: options.afterKey }); - } - } - - if (options.filterQuery) { - try { - const filterObject = JSON.parse(options.filterQuery); - if (isObject(filterObject)) { - params.body.query.bool.filter.push(filterObject); - } - } catch (err) { - params.body.query.bool.filter.push({ - query_string: { - query: options.filterQuery, - analyze_wildcard: true, - }, - }); - } - } - - const response = await search(params); - if (response.hits.total.value === 0) { - return { ...EMPTY_RESPONSE, series: [] }; - } - if (!response.aggregations) { - throw new Error('Aggregations should be present.'); - } - const { groupings, groupingsCount } = response.aggregations; - const { after_key: afterKey } = groupings; - return { - series: groupings.buckets.map((bucket) => { - const keys = Object.values(bucket.key); - const id = keys.join(' / '); - return { id, keys, rows: [], columns: [] }; - }), - pageInfo: { - total: groupingsCount.value, - afterKey: afterKey && groupings.buckets.length === limit ? afterKey : null, - }, - }; -}; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts deleted file mode 100644 index ce4a9c71b74e6b..00000000000000 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { union, uniq, isArray, isString } from 'lodash'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; -import { - MetricsExplorerRow, - MetricsExplorerSeries, - MetricsExplorerRequestBody, - MetricsExplorerColumn, -} from '../../../../common/http_api/metrics_explorer'; -import { createMetricModel } from './create_metrics_model'; -import { JsonObject } from '../../../../common/typed_json'; -import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; -import { getDatasetForField } from './get_dataset_for_field'; -import { - CallWithRequestParams, - InfraDatabaseSearchResponse, -} from '../../../lib/adapters/framework'; - -export const populateSeriesWithTSVBData = ( - request: KibanaRequest, - options: MetricsExplorerRequestBody, - framework: KibanaFramework, - requestContext: RequestHandlerContext -) => async (series: MetricsExplorerSeries) => { - // IF there are no metrics selected then we should return an empty result. - if (options.metrics.length === 0) { - return { - ...series, - columns: [], - rows: [], - }; - } - - // Set the filter for the group by or match everything - const isGroupBySet = - Array.isArray(options.groupBy) && options.groupBy.length - ? true - : isString(options.groupBy) - ? true - : false; - - const filters: JsonObject[] = isGroupBySet - ? isArray(options.groupBy) - ? options.groupBy - .filter((f) => f) - .map((field, index) => ({ match: { [field as string]: series.keys?.[index] || '' } })) - : [{ match: { [options.groupBy as string]: series.id } }] - : []; - - if (options.filterQuery) { - try { - const filterQuery = JSON.parse(options.filterQuery); - filters.push(filterQuery); - } catch (error) { - filters.push({ - query_string: { - query: options.filterQuery, - analyze_wildcard: true, - }, - }); - } - } - const timerange = { min: options.timerange.from, max: options.timerange.to }; - - const client = ( - opts: CallWithRequestParams - ): Promise> => - framework.callWithRequest(requestContext, 'search', opts); - - // Create the TSVB model based on the request options - const model = createMetricModel(options); - const modules = await Promise.all( - uniq(options.metrics.filter((m) => m.field)).map( - async (m) => await getDatasetForField(client, m.field as string, options.indexPattern) - ) - ); - - const calculatedInterval = await calculateMetricInterval( - client, - { - indexPattern: options.indexPattern, - timestampField: options.timerange.field, - timerange: options.timerange, - }, - modules.filter((m) => m) as string[] - ); - - if (calculatedInterval) { - model.interval = options.forceInterval - ? options.timerange.interval - : `>=${calculatedInterval}s`; - } - - // Get TSVB results using the model, timerange and filters - const tsvbResults = await framework.makeTSVBRequest( - requestContext, - request, - model, - timerange, - filters - ); - - // If there is no data `custom` will not exist. - if (!tsvbResults.custom) { - return { - ...series, - columns: [], - rows: [], - }; - } - - // Setup the dynamic columns and row attributes depending on if the user is doing a group by - // and multiple metrics - const attributeColumns: MetricsExplorerColumn[] = - options.groupBy != null ? [{ name: 'groupBy', type: 'string' }] : []; - const metricColumns: MetricsExplorerColumn[] = options.metrics.map((m, i) => ({ - name: `metric_${i}`, - type: 'number', - })); - const rowAttributes = options.groupBy != null ? { groupBy: series.id } : {}; - - // To support multiple metrics, there are multiple TSVB series which need to be combined - // into one MetricExplorerRow (Canvas row). This is done by collecting all the timestamps - // across each TSVB series. Then for each timestamp we find the values and create a - // MetricsExplorerRow. - const timestamps = tsvbResults.custom.series.reduce( - (currentTimestamps, tsvbSeries) => - union( - currentTimestamps, - tsvbSeries.data.map((row) => row[0]) - ).sort(), - [] as number[] - ); - // Combine the TSVB series for multiple metrics. - const rows = timestamps.map((timestamp) => { - return tsvbResults.custom.series.reduce( - (currentRow, tsvbSeries) => { - const matches = tsvbSeries.data.find((d) => d[0] === timestamp); - if (matches) { - return { ...currentRow, [tsvbSeries.id]: matches[1] }; - } - return currentRow; - }, - { timestamp, ...rowAttributes } as MetricsExplorerRow - ); - }); - return { - ...series, - rows, - columns: [ - { name: 'timestamp', type: 'date' } as MetricsExplorerColumn, - ...metricColumns, - ...attributeColumns, - ], - }; -}; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts new file mode 100644 index 00000000000000..25b956e093d0f9 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isArray } from 'lodash'; +import { MetricsAPIRequest } from '../../../../common/http_api'; +import { ESSearchClient } from '../../../lib/metrics/types'; + +interface GroupingResponse { + count: { + value: number; + }; +} + +export const queryTotalGroupings = async ( + client: ESSearchClient, + options: MetricsAPIRequest +): Promise => { + if (!options.groupBy || (isArray(options.groupBy) && options.groupBy.length === 0)) { + return Promise.resolve(0); + } + + const params = { + allowNoIndices: true, + ignoreUnavailable: true, + index: options.indexPattern, + body: { + size: 0, + query: { + bool: { + filter: [ + { + range: { + [options.timerange.field]: { + gte: options.timerange.from, + lte: options.timerange.to, + format: 'epoch_millis', + }, + }, + }, + ...options.groupBy.map((field) => ({ exists: { field } })), + ], + }, + }, + aggs: { + count: { + cardinality: { + script: options.groupBy.map((field) => `doc['${field}'].value`).join('+'), + }, + }, + }, + }, + }; + + const response = await client<{}, GroupingResponse>(params); + return response.aggregations?.count.value ?? 0; +}; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/transform_series.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/transform_series.ts new file mode 100644 index 00000000000000..227ce5c36c2dcf --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/transform_series.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPISeries, MetricsExplorerSeries } from '../../../../common/http_api'; + +export const transformSeries = (hasGroupBy: boolean) => ( + series: MetricsAPISeries +): MetricsExplorerSeries => { + const id = series.keys?.join(' / ') ?? series.id; + return { + ...series, + id, + rows: series.rows.map((row) => { + if (hasGroupBy) { + return { ...row, groupBy: id }; + } + return row; + }), + columns: hasGroupBy ? [...series.columns, { name: 'groupBy', type: 'string' }] : series.columns, + }; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 526f2d88574d34..5f52bfe9814406 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8891,7 +8891,7 @@ "xpack.infra.metrics.missingTSVBModelError": "{nodeType}では{metricId}のTSVBモデルが存在しません", "xpack.infra.metrics.pluginTitle": "メトリック", "xpack.infra.metrics.refetchButtonLabel": "新規データを確認", - "xpack.infra.metricsExploer.everything": "すべて", + "xpack.infra.metricsExplorer.everything": "すべて", "xpack.infra.metricsExplorer.actionsLabel.aria": "{grouping} のアクション", "xpack.infra.metricsExplorer.actionsLabel.button": "アクション", "xpack.infra.metricsExplorer.aggregationLabel": "/", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index af5e68b7e44d71..a97de80243deda 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8894,7 +8894,7 @@ "xpack.infra.metrics.missingTSVBModelError": "{nodeType} 的 {metricId} TSVB 模型不存在", "xpack.infra.metrics.pluginTitle": "指标", "xpack.infra.metrics.refetchButtonLabel": "检查新数据", - "xpack.infra.metricsExploer.everything": "所有内容", + "xpack.infra.metricsExplorer.everything": "所有内容", "xpack.infra.metricsExplorer.actionsLabel.aria": "适用于 {grouping} 的操作", "xpack.infra.metricsExplorer.actionsLabel.button": "操作", "xpack.infra.metricsExplorer.aggregationLabel": "的", diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts index d372496d2d1d96..16809fba8c8df2 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts @@ -50,7 +50,7 @@ export default function ({ getService }: FtrProviderContext) { const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(1); const firstSeries = first(body.series) as any; - expect(firstSeries).to.have.property('id', 'Everything'); + expect(firstSeries).to.have.property('id', '*'); expect(firstSeries.columns).to.eql([ { name: 'timestamp', type: 'date' }, { name: 'metric_0', type: 'number' }, @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(1); const firstSeries = first(body.series) as any; - expect(firstSeries).to.have.property('id', 'Everything'); + expect(firstSeries).to.have.property('id', '*'); expect(firstSeries.columns).to.eql([ { name: 'timestamp', type: 'date' }, { name: 'metric_0', type: 'number' }, @@ -121,7 +121,7 @@ export default function ({ getService }: FtrProviderContext) { const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); expect(body.series).length(1); const firstSeries = first(body.series) as any; - expect(firstSeries).to.have.property('id', 'Everything'); + expect(firstSeries).to.have.property('id', '*'); expect(firstSeries.columns).to.eql([]); expect(firstSeries.rows).to.have.length(0); }); From 4d6592edc8ad2b82f1a0eec98aeae810b63b1d73 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 14 Aug 2020 09:17:43 +0200 Subject: [PATCH 13/46] Revert "attempt excluding a codeowners directory" (#75023) This reverts commit 250a0b17b03f8924462d484c2254a5af7d64f1ff. --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5a8271302a72b9..1f076e3c840015 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -118,7 +118,6 @@ # Operations /src/dev/ @elastic/kibana-operations -!/src/dev/i18n/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations /src/optimize/ @elastic/kibana-operations /packages/*eslint*/ @elastic/kibana-operations From 7cf0e49c897512a827de870c3504888b29824499 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 14 Aug 2020 09:45:02 +0200 Subject: [PATCH 14/46] [Uptime] Singular alert (#74659) Co-authored-by: Elastic Machine --- .../translations/translations/ja-JP.json | 10 - .../translations/translations/zh-CN.json | 10 - .../uptime/common/runtime_types/ping/ping.ts | 6 + x-pack/plugins/uptime/common/translations.ts | 17 + .../__snapshots__/monitor_list.test.tsx.snap | 20 + .../monitor_list_drawer.test.tsx.snap | 5 + .../__tests__/monitor_status.test.ts | 4 +- .../public/lib/alert_types/monitor_status.tsx | 2 +- .../public/lib/alert_types/translations.ts | 13 - .../lib/alerts/__tests__/status_check.test.ts | 819 ++++++++++-------- .../server/lib/alerts/duration_anomaly.ts | 2 +- .../uptime/server/lib/alerts/status_check.ts | 609 +++++-------- .../uptime/server/lib/alerts/translations.ts | 77 ++ .../plugins/uptime/server/lib/alerts/types.ts | 2 +- .../server/lib/alerts/uptime_alert_wrapper.ts | 34 + .../uptime/server/lib/compose/kibana.ts | 14 +- .../get_monitor_availability.test.ts | 192 ++-- .../__tests__/get_monitor_status.test.ts | 148 ++-- .../lib/requests/get_monitor_availability.ts | 15 +- .../server/lib/requests/get_monitor_status.ts | 46 +- .../uptime/server/lib/requests/index.ts | 49 +- .../server/lib/requests/uptime_requests.ts | 57 -- 22 files changed, 1128 insertions(+), 1023 deletions(-) create mode 100644 x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts delete mode 100644 x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5f52bfe9814406..8c92e7359b2f7d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18868,10 +18868,6 @@ "xpack.uptime.alerts.anomaly.criteriaExpression.description": "監視するとき", "xpack.uptime.alerts.anomaly.scoreExpression.ariaLabel": "異常アラートしきい値の条件を表示する式。", "xpack.uptime.alerts.anomaly.scoreExpression.description": "異常と重要度があります", - "xpack.uptime.alerts.availability.emptyMessage": "可用性しきい値({threshold} %)未満のモニターはありません", - "xpack.uptime.alerts.availability.monitorSummary": "{nameOrId}({url}): {availabilityRatio}%", - "xpack.uptime.alerts.availability.multiItemTitle": "可用性しきい値({threshold} %)未満の上位{monitorCount}個のモニター:\n", - "xpack.uptime.alerts.availability.singleItemTitle": "可用性しきい値({threshold} %)未満のモニター:\n", "xpack.uptime.alerts.durationAnomaly": "アップタイム期間異常", "xpack.uptime.alerts.durationAnomaly.actionVariables.state.anomalyStartTimestamp": "異常の開始のISO8601タイムスタンプ", "xpack.uptime.alerts.durationAnomaly.actionVariables.state.expectedResponseTime": "想定応答時間", @@ -18884,11 +18880,6 @@ "xpack.uptime.alerts.durationAnomaly.actionVariables.state.slowestAnomalyResponse": "単位(ミリ秒、秒)が関連付けられた異常バケット中の最も遅い応答時間。", "xpack.uptime.alerts.durationAnomaly.clientName": "アップタイム期間異常", "xpack.uptime.alerts.durationAnomaly.defaultActionMessage": "{anomalyStartTimestamp}に、{monitor}、url {monitorUrl}で異常({severity}レベル)応答時間が検出されました。異常重要度スコアは{severityScore}です。\n位置情報{observerLocation}から高い応答時間{slowestAnomalyResponse}が検出されました。想定された応答時間は{expectedResponseTime}です。", - "xpack.uptime.alerts.message.emptyTitle": "停止状況監視 ID を受信していません。", - "xpack.uptime.alerts.message.fullListOverflow": "... とその他 {overflowCount} {pluralizedMonitor}", - "xpack.uptime.alerts.message.multipleTitle": "停止状況監視: ", - "xpack.uptime.alerts.message.overflowBody": "... とその他 {overflowCount} 監視", - "xpack.uptime.alerts.message.singularTitle": "停止状況監視: ", "xpack.uptime.alerts.monitorStatus": "稼働状況の監視ステータス", "xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description": "アラートによって「ダウン」と検知された一部またはすべてのモニターを示す、生成された概要。", "xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description": "現在ダウンしているモニターを要約する生成されたメッセージ。", @@ -18915,7 +18906,6 @@ "xpack.uptime.alerts.monitorStatus.availability.unit.headline": "時間範囲単位を選択します", "xpack.uptime.alerts.monitorStatus.availability.unit.selectable": "この選択を使用して、このアラートの可用性範囲単位を設定", "xpack.uptime.alerts.monitorStatus.clientName": "稼働状況の監視ステータス", - "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "{contextMessage}\n前回トリガー日時:{lastTriggered}\n", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "監視状態アラートのフィルター基準を許可するインプット", "xpack.uptime.alerts.monitorStatus.filters.anyLocation": "任意の場所", "xpack.uptime.alerts.monitorStatus.filters.anyPort": "任意のポート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a97de80243deda..5ab70ff7a9d04d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18876,10 +18876,6 @@ "xpack.uptime.alerts.anomaly.criteriaExpression.description": "当监测", "xpack.uptime.alerts.anomaly.scoreExpression.ariaLabel": "显示异常告警阈值的条件的表达式。", "xpack.uptime.alerts.anomaly.scoreExpression.description": "具有异常,严重性为", - "xpack.uptime.alerts.availability.emptyMessage": "没有监测低于可用性阈值 ({threshold} %)", - "xpack.uptime.alerts.availability.monitorSummary": "{nameOrId}({url}):{availabilityRatio}%", - "xpack.uptime.alerts.availability.multiItemTitle": "低于可用性阈值 ({threshold} %) 的排名前 {monitorCount} 监测:\n", - "xpack.uptime.alerts.availability.singleItemTitle": "低于可用性阈值 ({threshold} %) 的监测:\n", "xpack.uptime.alerts.durationAnomaly": "Uptime 持续时间异常", "xpack.uptime.alerts.durationAnomaly.actionVariables.state.anomalyStartTimestamp": "异常开始的 ISO8601 时间戳。", "xpack.uptime.alerts.durationAnomaly.actionVariables.state.expectedResponseTime": "预期响应时间", @@ -18892,11 +18888,6 @@ "xpack.uptime.alerts.durationAnomaly.actionVariables.state.slowestAnomalyResponse": "在附加单位(ms、s)的异常存储桶期间最慢的响应时间。", "xpack.uptime.alerts.durationAnomaly.clientName": "Uptime 持续时间异常", "xpack.uptime.alerts.durationAnomaly.defaultActionMessage": "{anomalyStartTimestamp} 在 url {monitorUrl} 的 {monitor} 上检测到异常({severity} 级别)响应时间。异常严重性分数为 {severityScore}。\n从位置 {observerLocation} 检测到高达 {slowestAnomalyResponse} 的响应时间。预期响应时间为 {expectedResponseTime}。", - "xpack.uptime.alerts.message.emptyTitle": "未接收到已关闭监测 ID", - "xpack.uptime.alerts.message.fullListOverflow": "...以及 {overflowCount} 个其他{pluralizedMonitor}", - "xpack.uptime.alerts.message.multipleTitle": "已关闭监测: ", - "xpack.uptime.alerts.message.overflowBody": "... 以及 {overflowCount} 个其他监测", - "xpack.uptime.alerts.message.singularTitle": "已关闭监测: ", "xpack.uptime.alerts.monitorStatus": "运行时间监测状态", "xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description": "生成的摘要,显示告警已检测为“关闭”的部分或所有监测", "xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description": "生成的消息,汇总当前关闭的监测", @@ -18923,7 +18914,6 @@ "xpack.uptime.alerts.monitorStatus.availability.unit.headline": "选择时间范围单位", "xpack.uptime.alerts.monitorStatus.availability.unit.selectable": "使用此选择来设置此告警的可用性范围单位", "xpack.uptime.alerts.monitorStatus.clientName": "运行时间监测状态", - "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "{contextMessage}\n上次触发时间:{lastTriggered}\n", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "允许对监测状态告警使用筛选条件的输入", "xpack.uptime.alerts.monitorStatus.filters.anyLocation": "任意位置", "xpack.uptime.alerts.monitorStatus.filters.anyPort": "任意端口", diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 0a4d6310927c47..f954f8ba308493 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -214,6 +214,9 @@ export const makePing = (f: { ip?: string; status?: string; duration?: number; + location?: string; + name?: string; + url?: string; }): Ping => { return { docId: f.docId || 'myDocId', @@ -224,7 +227,10 @@ export const makePing = (f: { ip: f.ip || '127.0.0.1', status: f.status || 'up', duration: { us: f.duration || 100000 }, + name: f.name, }, + ...(f.location ? { observer: { geo: { name: f.location } } } : {}), + ...(f.url ? { url: { full: f.url } } : {}), }; }; diff --git a/x-pack/plugins/uptime/common/translations.ts b/x-pack/plugins/uptime/common/translations.ts index 81f46df86f02e1..a4a20a1445f578 100644 --- a/x-pack/plugins/uptime/common/translations.ts +++ b/x-pack/plugins/uptime/common/translations.ts @@ -16,3 +16,20 @@ export const VALUE_MUST_BE_GREATER_THAN_ZERO = i18n.translate( export const VALUE_MUST_BE_AN_INTEGER = i18n.translate('xpack.uptime.settings.invalid.nanError', { defaultMessage: 'Value must be an integer.', }); + +export const MonitorStatusTranslations = { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.monitorStatus.defaultActionMessage', { + defaultMessage: + 'Monitor {monitorName} with url {monitorUrl} is {statusMessage} from {observerLocation}. The latest error message is {latestErrorMessage}', + values: { + monitorName: '{{state.monitorName}}', + monitorUrl: '{{{state.monitorUrl}}}', + statusMessage: '{{state.statusMessage}}', + latestErrorMessage: '{{{state.latestErrorMessage}}}', + observerLocation: '{{state.observerLocation}}', + }, + }), + name: i18n.translate('xpack.uptime.alerts.monitorStatus.clientName', { + defaultMessage: 'Uptime monitor status', + }), +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 42ac821c10c7a8..2ae8454b99893b 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -213,6 +213,7 @@ exports[`MonitorList component MonitorListPagination component renders the pagin }, "id": "foo", "ip": "127.0.0.1", + "name": undefined, "status": "up", "type": "icmp", }, @@ -226,6 +227,7 @@ exports[`MonitorList component MonitorListPagination component renders the pagin }, "id": "foo", "ip": "127.0.0.2", + "name": undefined, "status": "up", "type": "icmp", }, @@ -239,6 +241,7 @@ exports[`MonitorList component MonitorListPagination component renders the pagin }, "id": "foo", "ip": "127.0.0.3", + "name": undefined, "status": "down", "type": "icmp", }, @@ -266,6 +269,7 @@ exports[`MonitorList component MonitorListPagination component renders the pagin }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -279,6 +283,7 @@ exports[`MonitorList component MonitorListPagination component renders the pagin }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -516,6 +521,7 @@ exports[`MonitorList component renders error list 1`] = ` }, "id": "foo", "ip": "127.0.0.1", + "name": undefined, "status": "up", "type": "icmp", }, @@ -529,6 +535,7 @@ exports[`MonitorList component renders error list 1`] = ` }, "id": "foo", "ip": "127.0.0.2", + "name": undefined, "status": "up", "type": "icmp", }, @@ -542,6 +549,7 @@ exports[`MonitorList component renders error list 1`] = ` }, "id": "foo", "ip": "127.0.0.3", + "name": undefined, "status": "down", "type": "icmp", }, @@ -569,6 +577,7 @@ exports[`MonitorList component renders error list 1`] = ` }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -582,6 +591,7 @@ exports[`MonitorList component renders error list 1`] = ` }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -714,6 +724,7 @@ exports[`MonitorList component renders loading state 1`] = ` }, "id": "foo", "ip": "127.0.0.1", + "name": undefined, "status": "up", "type": "icmp", }, @@ -727,6 +738,7 @@ exports[`MonitorList component renders loading state 1`] = ` }, "id": "foo", "ip": "127.0.0.2", + "name": undefined, "status": "up", "type": "icmp", }, @@ -740,6 +752,7 @@ exports[`MonitorList component renders loading state 1`] = ` }, "id": "foo", "ip": "127.0.0.3", + "name": undefined, "status": "down", "type": "icmp", }, @@ -767,6 +780,7 @@ exports[`MonitorList component renders loading state 1`] = ` }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -780,6 +794,7 @@ exports[`MonitorList component renders loading state 1`] = ` }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -1611,6 +1626,7 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` }, "id": "foo", "ip": "127.0.0.1", + "name": undefined, "status": "up", "type": "icmp", }, @@ -1624,6 +1640,7 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` }, "id": "foo", "ip": "127.0.0.2", + "name": undefined, "status": "up", "type": "icmp", }, @@ -1637,6 +1654,7 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` }, "id": "foo", "ip": "127.0.0.3", + "name": undefined, "status": "down", "type": "icmp", }, @@ -1664,6 +1682,7 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -1677,6 +1696,7 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` }, "id": "bar", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap index 329fb8bade1065..42c885dfaf515f 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -113,6 +113,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are }, "id": "foo", "ip": "127.0.0.1", + "name": undefined, "status": "up", "type": "icmp", }, @@ -126,6 +127,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are }, "id": "foo", "ip": "127.0.0.1", + "name": undefined, "status": "down", "type": "icmp", }, @@ -139,6 +141,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are }, "id": "foo", "ip": "127.0.0.2", + "name": undefined, "status": "up", "type": "icmp", }, @@ -152,6 +155,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are }, "id": "foo", "ip": "127.0.0.3", + "name": undefined, "status": "down", "type": "icmp", }, @@ -284,6 +288,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there is o }, "id": "foo", "ip": "127.0.0.1", + "name": undefined, "status": "up", "type": "icmp", }, diff --git a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts index e999768d4e55d9..6af817c82ad950 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts @@ -202,9 +202,7 @@ describe('monitor status alert type', () => { ).toMatchInlineSnapshot(` Object { "alertParamsExpression": [Function], - "defaultActionMessage": "{{context.message}} - Last triggered at: {{state.lastTriggeredAt}} - ", + "defaultActionMessage": "Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}", "iconClass": "uptimeApp", "id": "xpack.uptime.alerts.monitorStatus", "name": { "certExpirationThreshold": 30, "heartbeatIndices": "heartbeat-8*", }, + "filters": undefined, "locations": Array [], "numTimes": 5, - "shouldCheckStatus": true, "timerange": Object { "from": "now-15m", "to": "now", @@ -114,19 +114,28 @@ describe('status check alert', () => { it('triggers when monitors are down and provides expected state', async () => { toISOStringSpy.mockImplementation(() => 'foo date string'); - const mockGetter = jest.fn(); + const mockGetter: jest.Mock = jest.fn(); + mockGetter.mockReturnValue([ { - monitor_id: 'first', + monitorId: 'first', location: 'harrisburg', count: 234, status: 'down', + monitorInfo: makePing({ + id: 'first', + location: 'harrisburg', + }), }, { - monitor_id: 'first', + monitorId: 'first', location: 'fairbanks', count: 234, status: 'down', + monitorInfo: makePing({ + id: 'first', + location: 'fairbanks', + }), }, ]); const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); @@ -136,7 +145,7 @@ describe('status check alert', () => { // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledTimes(1); + expect(alertServices.alertInstanceFactory).toHaveBeenCalledTimes(2); expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -146,9 +155,9 @@ describe('status check alert', () => { "certExpirationThreshold": 30, "heartbeatIndices": "heartbeat-8*", }, + "filters": undefined, "locations": Array [], "numTimes": 5, - "shouldCheckStatus": true, "timerange": Object { "from": "now-15m", "to": "now", @@ -157,7 +166,7 @@ describe('status check alert', () => { ] `); const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -168,30 +177,23 @@ describe('status check alert', () => { "lastCheckedAt": "foo date string", "lastResolvedAt": undefined, "lastTriggeredAt": "foo date string", - "monitors": Array [ - Object { - "count": 234, - "location": "fairbanks", - "monitor_id": "first", - "status": "down", - }, - Object { - "count": 234, - "location": "harrisburg", - "monitor_id": "first", - "status": "down", - }, - ], + "latestErrorMessage": undefined, + "monitorId": "first", + "monitorName": "first", + "monitorType": "myType", + "monitorUrl": undefined, + "observerHostname": undefined, + "observerLocation": "harrisburg", + "statusMessage": "down", }, ] `); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2); expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { - "downMonitorsWithGeo": "first from fairbanks; first from harrisburg; ", - "message": "Down monitor: first", + "message": "Monitor first with url is down from harrisburg. The latest error message is ", }, ] `); @@ -199,19 +201,29 @@ describe('status check alert', () => { it('supports 7.7 alert format', async () => { toISOStringSpy.mockImplementation(() => '7.7 date'); - const mockGetter = jest.fn(); + const mockGetter: jest.Mock = jest.fn(); + mockGetter.mockReturnValue([ { - monitor_id: 'first', + monitorId: 'first', location: 'harrisburg', count: 234, status: 'down', + monitorInfo: makePing({ + id: 'first', + location: 'harrisburg', + }), }, { - monitor_id: 'first', + monitorId: 'first', location: 'fairbanks', count: 234, status: 'down', + + monitorInfo: makePing({ + id: 'first', + location: 'fairbanks', + }), }, ]); const { server, libs, plugins } = bootstrapDependencies({ @@ -227,8 +239,9 @@ describe('status check alert', () => { }); const alertServices: AlertServicesMock = options.services; const state = await alert.executor(options); + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -239,20 +252,14 @@ describe('status check alert', () => { "lastCheckedAt": "7.7 date", "lastResolvedAt": undefined, "lastTriggeredAt": "7.7 date", - "monitors": Array [ - Object { - "count": 234, - "location": "fairbanks", - "monitor_id": "first", - "status": "down", - }, - Object { - "count": 234, - "location": "harrisburg", - "monitor_id": "first", - "status": "down", - }, - ], + "latestErrorMessage": undefined, + "monitorId": "first", + "monitorName": "first", + "monitorType": "myType", + "monitorUrl": undefined, + "observerHostname": undefined, + "observerLocation": "harrisburg", + "statusMessage": "down", }, ] `); @@ -272,19 +279,28 @@ describe('status check alert', () => { it('supports 7.8 alert format', async () => { expect.assertions(5); toISOStringSpy.mockImplementation(() => 'foo date string'); - const mockGetter = jest.fn(); + const mockGetter: jest.Mock = jest.fn(); mockGetter.mockReturnValue([ { - monitor_id: 'first', + monitorId: 'first', location: 'harrisburg', count: 234, status: 'down', + monitorInfo: makePing({ + id: 'first', + location: 'harrisburg', + }), }, { - monitor_id: 'first', + monitorId: 'first', location: 'fairbanks', count: 234, status: 'down', + + monitorInfo: makePing({ + id: 'first', + location: 'fairbanks', + }), }, ]); const { server, libs, plugins } = bootstrapDependencies({ @@ -317,7 +333,166 @@ describe('status check alert', () => { "certExpirationThreshold": 30, "heartbeatIndices": "heartbeat-8*", }, - "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":12349}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":5601}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":443}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}]}}]}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"monitor.ip\\"}}],\\"minimum_should_match\\":1}}]}}", + "filters": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "url.port": 12349, + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "url.port": 5601, + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "url.port": 443, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "harrisburg", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "monitor.type": "http", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "tags": "unsecured", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "tags": "containers", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "tags": "org:google", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "monitor.ip", + }, + }, + ], + }, + }, + ], + }, + }, "locations": Array [], "numTimes": 3, "timerange": Object { @@ -327,7 +502,7 @@ describe('status check alert', () => { }, ] `); - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -338,20 +513,14 @@ describe('status check alert', () => { "lastCheckedAt": "foo date string", "lastResolvedAt": undefined, "lastTriggeredAt": "foo date string", - "monitors": Array [ - Object { - "count": 234, - "location": "fairbanks", - "monitor_id": "first", - "status": "down", - }, - Object { - "count": 234, - "location": "harrisburg", - "monitor_id": "first", - "status": "down", - }, - ], + "latestErrorMessage": undefined, + "monitorId": "first", + "monitorName": "first", + "monitorType": "myType", + "monitorUrl": undefined, + "observerHostname": undefined, + "observerLocation": "harrisburg", + "statusMessage": "down", }, ] `); @@ -390,6 +559,7 @@ describe('status check alert', () => { search: 'url.full: *', }); await alert.executor(options); + expect(mockGetter).toHaveBeenCalledTimes(1); expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -400,7 +570,36 @@ describe('status check alert', () => { "certExpirationThreshold": 30, "heartbeatIndices": "heartbeat-8*", }, - "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"url.full\\"}}],\\"minimum_should_match\\":1}}]}}", + "filters": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "monitor.type": "http", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "url.full", + }, + }, + ], + }, + }, + ], + }, + }, "locations": Array [], "numTimes": 20, "timerange": Object { @@ -415,57 +614,60 @@ describe('status check alert', () => { it('supports availability checks', async () => { expect.assertions(8); toISOStringSpy.mockImplementation(() => 'availability test'); - const mockGetter = jest.fn(); - mockGetter.mockReturnValue([ - { - monitor_id: 'first', - location: 'harrisburg', - count: 234, - status: 'down', - }, - { - monitor_id: 'first', - location: 'fairbanks', - count: 234, - status: 'down', - }, - ]); - const mockAvailability = jest.fn(); + const mockGetter: jest.Mock = jest.fn(); + mockGetter.mockReturnValue([]); + const mockAvailability: jest.Mock = jest.fn(); mockAvailability.mockReturnValue([ { monitorId: 'foo', location: 'harrisburg', - name: 'Foo', - url: 'https://foo.com', up: 2341, down: 17, availabilityRatio: 0.992790500424088, + monitorInfo: makePing({ + id: 'foo', + location: 'harrisburg', + name: 'Foo', + url: 'https://foo.com', + }), }, { monitorId: 'foo', location: 'fairbanks', - name: 'Foo', - url: 'https://foo.com', up: 2343, down: 47, availabilityRatio: 0.980334728033473, + monitorInfo: makePing({ + id: 'foo', + location: 'fairbanks', + name: 'Foo', + url: 'https://foo.com', + }), }, { monitorId: 'unreliable', location: 'fairbanks', - name: 'Unreliable', - url: 'https://unreliable.co', up: 2134, down: 213, availabilityRatio: 0.909245845760545, + monitorInfo: makePing({ + id: 'unreliable', + location: 'fairbanks', + name: 'Unreliable', + url: 'https://unreliable.co', + }), }, { monitorId: 'no-name', location: 'fairbanks', - url: 'https://no-name.co', up: 2134, down: 213, availabilityRatio: 0.909245845760545, + monitorInfo: makePing({ + id: 'no-name', + location: 'fairbanks', + url: 'https://no-name.co', + }), }, ]); const { server, libs, plugins } = bootstrapDependencies({ @@ -487,11 +689,12 @@ describe('status check alert', () => { tags: ['unsecured', 'containers', 'org:google'], }, shouldCheckAvailability: true, + shouldCheckStatus: false, }); const alertServices: AlertServicesMock = options.services; const state = await alert.executor(options); const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -502,22 +705,42 @@ describe('status check alert', () => { "lastCheckedAt": "availability test", "lastResolvedAt": undefined, "lastTriggeredAt": "availability test", - "monitors": Array [], + "latestErrorMessage": undefined, + "monitorId": "foo", + "monitorName": "Foo", + "monitorType": "myType", + "monitorUrl": "https://foo.com", + "observerHostname": undefined, + "observerLocation": "harrisburg", + "statusMessage": "below threshold with 99.28% availability expected is 99.34%", }, ] `); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4); expect(alertInstanceMock.scheduleActions.mock.calls).toMatchInlineSnapshot(` Array [ Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { - "downMonitorsWithGeo": "", - "message": "Top 3 Monitors Below Availability Threshold (99.34 %): - Unreliable(https://unreliable.co): 90.925% - no-name(https://no-name.co): 90.925% - Foo(https://foo.com): 98.033% - ", + "message": "Monitor Foo with url https://foo.com is below threshold with 99.28% availability expected is 99.34% from harrisburg. The latest error message is ", + }, + ], + Array [ + "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "message": "Monitor Foo with url https://foo.com is below threshold with 98.03% availability expected is 99.34% from fairbanks. The latest error message is ", + }, + ], + Array [ + "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "message": "Monitor Unreliable with url https://unreliable.co is below threshold with 90.92% availability expected is 99.34% from fairbanks. The latest error message is ", + }, + ], + Array [ + "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "message": "Monitor no-name with url https://no-name.co is below threshold with 90.92% availability expected is 99.34% from fairbanks. The latest error message is ", }, ], ] @@ -573,7 +796,9 @@ describe('status check alert', () => { }, search: 'ur.port: *', shouldCheckAvailability: true, + shouldCheckStatus: false, }); + await alert.executor(options); expect(mockAvailability).toHaveBeenCalledTimes(1); expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` @@ -613,8 +838,11 @@ describe('status check alert', () => { threshold: '90', }, shouldCheckAvailability: true, + shouldCheckStatus: false, }); + await alert.executor(options); + expect(mockAvailability).toHaveBeenCalledTimes(1); expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -635,116 +863,6 @@ describe('status check alert', () => { }); }); - describe('fullListByIdAndLocation', () => { - it('renders a list of all monitors', () => { - const statuses: GetMonitorStatusResult[] = [ - { - location: 'harrisburg', - monitor_id: 'first', - status: 'down', - count: 34, - }, - { - location: 'fairbanks', - monitor_id: 'second', - status: 'down', - count: 23, - }, - { - location: 'fairbanks', - monitor_id: 'first', - status: 'down', - count: 23, - }, - { - location: 'harrisburg', - monitor_id: 'second', - status: 'down', - count: 34, - }, - ]; - expect(fullListByIdAndLocation(statuses)).toMatchInlineSnapshot( - `"first from fairbanks; first from harrisburg; second from fairbanks; second from harrisburg; "` - ); - }); - - it('renders a list of monitors when greater than limit', () => { - const statuses: GetMonitorStatusResult[] = [ - { - location: 'fairbanks', - monitor_id: 'second', - status: 'down', - count: 23, - }, - { - location: 'fairbanks', - monitor_id: 'first', - status: 'down', - count: 23, - }, - { - location: 'harrisburg', - monitor_id: 'first', - status: 'down', - count: 34, - }, - { - location: 'harrisburg', - monitor_id: 'second', - status: 'down', - count: 34, - }, - ]; - expect(fullListByIdAndLocation(statuses.slice(0, 2), 1)).toMatchInlineSnapshot( - `"first from fairbanks; ...and 1 other monitor/location"` - ); - }); - - it('renders expected list of monitors when limit difference > 1', () => { - const statuses: GetMonitorStatusResult[] = [ - { - location: 'fairbanks', - monitor_id: 'second', - status: 'down', - count: 23, - }, - { - location: 'harrisburg', - monitor_id: 'first', - status: 'down', - count: 34, - }, - { - location: 'harrisburg', - monitor_id: 'second', - status: 'down', - count: 34, - }, - { - location: 'harrisburg', - monitor_id: 'third', - status: 'down', - count: 34, - }, - { - location: 'fairbanks', - monitor_id: 'third', - status: 'down', - count: 23, - }, - { - location: 'fairbanks', - monitor_id: 'first', - status: 'down', - count: 23, - }, - ]; - expect(fullListByIdAndLocation(statuses, 4)).toMatchInlineSnapshot( - `"first from fairbanks; first from harrisburg; second from fairbanks; second from harrisburg; ...and 2 other monitors/locations"` - ); - }); - }); - describe('alert factory', () => { let alert: AlertType; @@ -820,16 +938,26 @@ describe('status check alert', () => { mockGetIndexPattern.mockReturnValue(undefined); it('returns `undefined` for no filters or search', async () => { - expect(await generateFilterDSL(mockGetIndexPattern)).toBeUndefined(); + expect( + await generateFilterDSL( + mockGetIndexPattern, + { 'monitor.type': [], 'observer.geo.name': [], tags: [], 'url.port': [] }, + '' + ) + ).toBeUndefined(); }); it('creates a filter string for filters only', async () => { - const res = await generateFilterDSL(mockGetIndexPattern, { - 'monitor.type': [], - 'observer.geo.name': ['us-east', 'us-west'], - tags: [], - 'url.port': [], - }); + const res = await generateFilterDSL( + mockGetIndexPattern, + { + 'monitor.type': [], + 'observer.geo.name': ['us-east', 'us-west'], + tags: [], + 'url.port': [], + }, + '' + ); expect(res).toMatchInlineSnapshot(` Object { "bool": Object { @@ -866,8 +994,13 @@ describe('status check alert', () => { }); it('creates a filter string for search only', async () => { - expect(await generateFilterDSL(mockGetIndexPattern, undefined, 'monitor.id: "kibana-dev"')) - .toMatchInlineSnapshot(` + expect( + await generateFilterDSL( + mockGetIndexPattern, + { 'monitor.type': [], 'observer.geo.name': [], tags: [], 'url.port': [] }, + 'monitor.id: "kibana-dev"' + ) + ).toMatchInlineSnapshot(` Object { "bool": Object { "minimum_should_match": 1, @@ -987,217 +1120,159 @@ describe('status check alert', () => { }); describe('uniqueMonitorIds', () => { - let items: GetMonitorStatusResult[]; + let downItems: GetMonitorStatusResult[]; + let availItems: GetMonitorAvailabilityResult[]; beforeEach(() => { - items = [ + downItems = [ { - monitor_id: 'first', + monitorId: 'first', location: 'harrisburg', count: 234, status: 'down', + monitorInfo: makePing({}), }, { - monitor_id: 'first', + monitorId: 'first', location: 'fairbanks', count: 312, status: 'down', + monitorInfo: makePing({}), }, { - monitor_id: 'second', + monitorId: 'second', location: 'harrisburg', count: 325, status: 'down', + monitorInfo: makePing({}), }, { - monitor_id: 'second', + monitorId: 'second', location: 'fairbanks', count: 331, status: 'down', + monitorInfo: makePing({}), }, + ]; + + availItems = [ { - monitor_id: 'third', - location: 'harrisburg', - count: 331, - status: 'down', - }, - { - monitor_id: 'third', - location: 'fairbanks', - count: 342, - status: 'down', - }, - { - monitor_id: 'fourth', + monitorId: 'first', location: 'harrisburg', - count: 355, - status: 'down', + monitorInfo: makePing({}), + up: 2134, + down: 213, + availabilityRatio: 0.909245845760545, }, { - monitor_id: 'fourth', + monitorId: 'first', location: 'fairbanks', - count: 342, - status: 'down', + monitorInfo: makePing({}), + up: 2134, + down: 213, + availabilityRatio: 0.909245845760545, }, { - monitor_id: 'fifth', + monitorId: 'second', location: 'harrisburg', - count: 342, - status: 'down', + monitorInfo: makePing({}), + up: 2134, + down: 213, + availabilityRatio: 0.909245845760545, }, { - monitor_id: 'fifth', + monitorId: 'second', location: 'fairbanks', - count: 342, - status: 'down', + monitorInfo: makePing({}), + up: 2134, + down: 213, + availabilityRatio: 0.909245845760545, }, ]; }); it('creates a set of unique IDs from a list of composite unique objects', () => { - expect(uniqueMonitorIds(items)).toEqual( - new Set(['first', 'second', 'third', 'fourth', 'fifth']) - ); - }); - }); - - describe('contextMessage', () => { - let ids: string[]; - beforeEach(() => { - ids = ['first', 'second', 'third', 'fourth', 'fifth']; - }); - - it('creates a message with appropriate number of monitors', () => { - expect(contextMessage(ids, 3, [], '0', false, true)).toMatchInlineSnapshot( - `"Down monitors: first, second, third... and 2 other monitors"` - ); - }); - - it('throws an error if `max` is less than 2', () => { - expect(() => contextMessage(ids, 1, [], '0', false, true)).toThrowErrorMatchingInlineSnapshot( - '"Maximum value must be greater than 2, received 1."' - ); - }); - - it('returns only the ids if length < max', () => { - expect(contextMessage(ids.slice(0, 2), 3, [], '0', false, true)).toMatchInlineSnapshot( - `"Down monitors: first, second"` - ); - }); - - it('returns a default message when no monitors are provided', () => { - expect(contextMessage([], 3, [], '0', false, true)).toMatchInlineSnapshot( - `"No down monitor IDs received"` + expect(getUniqueIdsByLoc(downItems, availItems)).toEqual( + new Set([ + 'firstharrisburg', + 'firstfairbanks', + 'secondharrisburg', + 'secondfairbanks', + ]) ); }); }); - describe('availabilityMessage', () => { - it('creates message for singular item', () => { + describe('statusMessage', () => { + it('creates message for down item', () => { expect( - availabilityMessage( - [ - { - monitorId: 'test-node-service', - location: 'fairbanks', - name: 'Test Node Service', - url: 'http://localhost:12349', - up: 821.0, - down: 2450.0, - availabilityRatio: 0.25099357994497096, - }, - ], - '59' + getStatusMessage( + makePing({ + id: 'test-node-service', + location: 'fairbanks', + name: 'Test Node Service', + url: 'http://localhost:12349', + }) ) - ).toMatchInlineSnapshot(` - "Monitor Below Availability Threshold (59 %): - Test Node Service(http://localhost:12349): 25.099% - " - `); + ).toMatchInlineSnapshot(`"down"`); }); - it('creates message for multiple items', () => { + it('creates message for availability item', () => { expect( - availabilityMessage( - [ - { - monitorId: 'test-node-service', - location: 'fairbanks', + getStatusMessage( + undefined, + { + monitorId: 'test-node-service', + location: 'harrisburg', + up: 3389.0, + down: 2450.0, + availabilityRatio: 0.5804076040417879, + monitorInfo: makePing({ name: 'Test Node Service', url: 'http://localhost:12349', - up: 821.0, - down: 2450.0, - availabilityRatio: 0.25099357994497096, - }, - { - monitorId: 'test-node-service', + id: 'test-node-service', location: 'harrisburg', - name: 'Test Node Service', - url: 'http://localhost:12349', - up: 3389.0, - down: 2450.0, - availabilityRatio: 0.5804076040417879, - }, - ], - '59' + }), + }, + { + threshold: '90', + range: 5, + rangeUnit: 'm', + } ) - ).toMatchInlineSnapshot(` - "Top 2 Monitors Below Availability Threshold (59 %): - Test Node Service(http://localhost:12349): 25.099% - Test Node Service(http://localhost:12349): 58.041% - " - `); + ).toMatchInlineSnapshot(`"below threshold with 58.04% availability expected is 90%"`); }); - it('caps message for multiple items', () => { + it('creates message for down and availability item', () => { expect( - availabilityMessage( - [ - { - monitorId: 'test-node-service', - location: 'fairbanks', + getStatusMessage( + makePing({ + id: 'test-node-service', + location: 'fairbanks', + name: 'Test Node Service', + url: 'http://localhost:12349', + }), + { + monitorId: 'test-node-service', + location: 'harrisburg', + up: 3389.0, + down: 2450.0, + availabilityRatio: 0.5804076040417879, + monitorInfo: makePing({ name: 'Test Node Service', url: 'http://localhost:12349', - up: 821.0, - down: 2450.0, - availabilityRatio: 0.250993579944971, - }, - { - monitorId: 'test-node-service', + id: 'test-node-service', location: 'harrisburg', - name: 'Test Node Service', - url: 'http://localhost:12349', - up: 3389.0, - down: 2450.0, - availabilityRatio: 0.58040760404178, - }, - { - monitorId: 'test-node-service', - location: 'berlin', - name: 'Test Node Service', - url: 'http://localhost:12349', - up: 3645.0, - down: 2982.0, - availabilityRatio: 0.550022634676324, - }, - { - monitorId: 'test-node-service', - location: 'st paul', - name: 'Test Node Service', - url: 'http://localhost:12349', - up: 3601.0, - down: 2681.0, - availabilityRatio: 0.573225087551735, - }, - ], - '59' + }), + }, + { + threshold: '90', + range: 5, + rangeUnit: 'm', + } ) - ).toMatchInlineSnapshot(` - "Top 3 Monitors Below Availability Threshold (59 %): - Test Node Service(http://localhost:12349): 25.099% - Test Node Service(http://localhost:12349): 55.002% - Test Node Service(http://localhost:12349): 57.323% - " - `); + ).toMatchInlineSnapshot( + `"down and also below threshold with 58.04% availability expected is 90%"` + ); }); }); }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index a71913d0eea9af..9ed453d2862854 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -12,12 +12,12 @@ import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; -import { getLatestMonitor } from '../requests'; import { savedObjectsAdapter } from '../saved_objects'; import { UptimeCorePlugins } from '../adapters/framework'; import { UptimeAlertTypeFactory } from './types'; import { Ping } from '../../../common/runtime_types/ping'; import { getMLJobId } from '../../../common/lib'; +import { getLatestMonitor } from '../requests/get_latest_monitor'; const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 8ca2e857a52c9f..134472ba0693fa 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -5,225 +5,46 @@ */ import { schema } from '@kbn/config-schema'; -import { isRight } from 'fp-ts/lib/Either'; -import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { i18n } from '@kbn/i18n'; -import { AlertExecutorOptions } from '../../../../alerts/server'; +import { ILegacyScopedClusterClient } from 'kibana/server'; +import Mustache from 'mustache'; import { UptimeAlertTypeFactory } from './types'; -import { GetMonitorStatusResult } from '../requests'; import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common'; import { - StatusCheckParamsType, - StatusCheckParams, StatusCheckFilters, - AtomicStatusCheckParamsType, - MonitorAvailabilityType, DynamicSettings, + Ping, + GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; -import { savedObjectsAdapter } from '../saved_objects'; import { updateState } from './common'; -import { commonStateTranslations } from './translations'; +import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations'; import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib'; import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability'; import { UMServerLibs } from '../lib'; +import { GetMonitorStatusResult } from '../requests/get_monitor_status'; +import { UNNAMED_LOCATION } from '../../../common/constants'; +import { uptimeAlertWrapper } from './uptime_alert_wrapper'; +import { MonitorStatusTranslations } from '../../../common/translations'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; -/** - * Reduce a composite-key array of status results to a set of unique IDs. - * @param items to reduce - */ -export const uniqueMonitorIds = (items: GetMonitorStatusResult[]): Set => - // eslint-disable-next-line @typescript-eslint/naming-convention - items.reduce((acc, { monitor_id }) => { - acc.add(monitor_id); - return acc; - }, new Set()); - -const sortAvailabilityResultByRatioAsc = ( - a: GetMonitorAvailabilityResult, - b: GetMonitorAvailabilityResult -): number => (a.availabilityRatio ?? 100) - (b.availabilityRatio ?? 100); - -/** - * Map an availability result object to a descriptive string. - */ -const mapAvailabilityResultToString = ({ - availabilityRatio, - name, - monitorId, - url, -}: GetMonitorAvailabilityResult) => - i18n.translate('xpack.uptime.alerts.availability.monitorSummary', { - defaultMessage: '{nameOrId}({url}): {availabilityRatio}%', - values: { - nameOrId: name || monitorId, - url, - availabilityRatio: ((availabilityRatio ?? 1.0) * 100).toPrecision(5), - }, - }); - -const reduceAvailabilityStringsToMessage = (threshold: string) => ( - prev: string, - cur: string, - _ind: number, - array: string[] -) => { - let prefix: string = ''; - if (prev !== '') { - prefix = prev; - } else if (array.length > 1) { - prefix = i18n.translate('xpack.uptime.alerts.availability.multiItemTitle', { - defaultMessage: `Top {monitorCount} Monitors Below Availability Threshold ({threshold} %):\n`, - values: { - monitorCount: Math.min(array.length, MESSAGE_AVAILABILITY_MAX), - threshold, - }, - }); - } else { - prefix = i18n.translate('xpack.uptime.alerts.availability.singleItemTitle', { - defaultMessage: `Monitor Below Availability Threshold ({threshold} %):\n`, - values: { threshold }, - }); - } - return prefix + `${cur}\n`; -}; - -const MESSAGE_AVAILABILITY_MAX = 3; - -/** - * Creates a summary message from a list of availability check result objects. - * @param availabilityResult the list of results - * @param threshold the threshold used by the check - */ -export const availabilityMessage = ( - availabilityResult: GetMonitorAvailabilityResult[], - threshold: string, - max: number = MESSAGE_AVAILABILITY_MAX -): string => { - return availabilityResult.length > 0 - ? // if there are results, map each item to a descriptive string, and reduce the list - availabilityResult - .sort(sortAvailabilityResultByRatioAsc) - .slice(0, max) - .map(mapAvailabilityResultToString) - .reduce(reduceAvailabilityStringsToMessage(threshold), '') - : // if there are no results, return an empty list default string - i18n.translate('xpack.uptime.alerts.availability.emptyMessage', { - defaultMessage: `No monitors were below Availability Threshold ({threshold} %)`, - values: { - threshold, - }, - }); -}; - -/** - * Generates a message to include in contexts of alerts. - * @param monitors the list of monitors to include in the message - * @param max the maximum number of items the summary should contain - */ -export const contextMessage = ( - monitorIds: string[], - max: number, - availabilityResult: GetMonitorAvailabilityResult[], - availabilityThreshold: string, - availabilityWasChecked: boolean, - statusWasChecked: boolean -): string => { - const MIN = 2; - if (max < MIN) throw new Error(`Maximum value must be greater than ${MIN}, received ${max}.`); - - // generate the message - let message = ''; - if (statusWasChecked) { - if (monitorIds.length === 1) { - message = i18n.translate('xpack.uptime.alerts.message.singularTitle', { - defaultMessage: 'Down monitor: ', - }); - } else if (monitorIds.length) { - message = i18n.translate('xpack.uptime.alerts.message.multipleTitle', { - defaultMessage: 'Down monitors: ', - }); - } else { - message = i18n.translate('xpack.uptime.alerts.message.emptyTitle', { - defaultMessage: 'No down monitor IDs received', - }); - } - - for (let i = 0; i < monitorIds.length; i++) { - const id = monitorIds[i]; - if (i === max) { - message = - message + - i18n.translate('xpack.uptime.alerts.message.overflowBody', { - defaultMessage: `... and {overflowCount} other monitors`, - values: { - overflowCount: monitorIds.length - i, - }, - }); - break; - } else if (i === 0) { - message = message + id; - } else { - message = message + `, ${id}`; - } - } - } - - if (availabilityWasChecked) { - const availabilityMsg = availabilityMessage(availabilityResult, availabilityThreshold); - return message ? message + '\n' + availabilityMsg : availabilityMsg; - } +const uniqueDownMonitorIds = (items: GetMonitorStatusResult[]): Set => + items.reduce((acc, { monitorId, location }) => acc.add(monitorId + location), new Set()); - return message; -}; +const uniqueAvailMonitorIds = (items: GetMonitorAvailabilityResult[]): Set => + items.reduce((acc, { monitorId, location }) => acc.add(monitorId + location), new Set()); -/** - * Creates an exhaustive list of all the down monitors. - * @param list all the monitors that are down - * @param sizeLimit the max monitors, we shouldn't allow an arbitrarily long string - */ -export const fullListByIdAndLocation = ( - list: GetMonitorStatusResult[], - sizeLimit: number = 1000 +export const getUniqueIdsByLoc = ( + downMonitorsByLocation: GetMonitorStatusResult[], + availabilityResults: GetMonitorAvailabilityResult[] ) => { - return ( - list - // sort by id, then location - .sort((a, b) => { - if (a.monitor_id > b.monitor_id) { - return 1; - } else if (a.monitor_id < b.monitor_id) { - return -1; - } else if (a.location > b.location) { - return 1; - } - return -1; - }) - .slice(0, sizeLimit) - .reduce( - (cur, { monitor_id: id, location }) => - cur + `${id} from ${location ?? 'Unnamed location'}; `, - '' - ) + - (sizeLimit < list.length - ? i18n.translate('xpack.uptime.alerts.message.fullListOverflow', { - defaultMessage: '...and {overflowCount} other {pluralizedMonitor}', - values: { - pluralizedMonitor: - list.length - sizeLimit === 1 ? 'monitor/location' : 'monitors/locations', - overflowCount: list.length - sizeLimit, - }, - }) - : '') - ); -}; + const uniqueDownsIdsByLoc = uniqueDownMonitorIds(downMonitorsByLocation); + const uniqueAvailIdsByLoc = uniqueAvailMonitorIds(availabilityResults); -// Right now the maximum number of monitors shown in the message is hardcoded here. -// we might want to make this a parameter in the future -const DEFAULT_MAX_MESSAGE_ROWS = 3; + return new Set([...uniqueDownsIdsByLoc, ...uniqueAvailIdsByLoc]); +}; export const hasFilters = (filters?: StatusCheckFilters) => { if (!filters) return false; @@ -237,25 +58,18 @@ export const hasFilters = (filters?: StatusCheckFilters) => { export const generateFilterDSL = async ( getIndexPattern: () => Promise, - filters?: StatusCheckFilters, - search?: string + filters: StatusCheckFilters, + search: string ): Promise => { const filtersExist = hasFilters(filters); if (!filtersExist && !search) return undefined; - let filterString: string | undefined; + let filterString = ''; if (filtersExist) { filterString = stringifyKueries(new Map(Object.entries(filters ?? {}))); } - let combinedString: string | undefined; - if (filterString && search) { - combinedString = combineFiltersAndUserSearch(filterString, search); - } else if (filterString) { - combinedString = filterString; - } else if (search) { - combinedString = search; - } + const combinedString = combineFiltersAndUserSearch(filterString, search); return esKuery.toElasticsearchQuery( esKuery.fromKueryExpression(combinedString ?? ''), @@ -266,183 +80,232 @@ export const generateFilterDSL = async ( const formatFilterString = async ( libs: UMServerLibs, dynamicSettings: DynamicSettings, - options: AlertExecutorOptions, - filters?: StatusCheckFilters, - search?: string + callES: ILegacyScopedClusterClient['callAsCurrentUser'], + filters: StatusCheckFilters, + search: string ) => - JSON.stringify( - await generateFilterDSL( - () => - libs.requests.getIndexPattern({ - callES: options.services.callCluster, - dynamicSettings, - }), - filters, - search - ) + await generateFilterDSL( + () => + libs.requests.getIndexPattern({ + callES, + dynamicSettings, + }), + filters, + search ); -export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ - id: 'xpack.uptime.alerts.monitorStatus', - name: i18n.translate('xpack.uptime.alerts.monitorStatus', { - defaultMessage: 'Uptime monitor status', - }), - validate: { - params: schema.object({ - availability: schema.maybe( - schema.object({ - range: schema.number(), - rangeUnit: schema.string(), - threshold: schema.string(), - }) - ), - filters: schema.maybe( - schema.oneOf([ - // deprecated - schema.object({ - 'monitor.type': schema.maybe(schema.arrayOf(schema.string())), - 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())), - tags: schema.maybe(schema.arrayOf(schema.string())), - 'url.port': schema.maybe(schema.arrayOf(schema.string())), - }), - schema.string(), - ]) - ), - // deprecated - locations: schema.maybe(schema.arrayOf(schema.string())), - numTimes: schema.number(), - search: schema.maybe(schema.string()), - shouldCheckStatus: schema.maybe(schema.boolean()), - shouldCheckAvailability: schema.maybe(schema.boolean()), - timerangeCount: schema.maybe(schema.number()), - timerangeUnit: schema.maybe(schema.string()), - // deprecated - timerange: schema.maybe( - schema.object({ - from: schema.string(), - to: schema.string(), - }) - ), - version: schema.maybe(schema.number()), - }), - }, - defaultActionGroupId: MONITOR_STATUS.id, - actionGroups: [ - { - id: MONITOR_STATUS.id, - name: MONITOR_STATUS.name, - }, - ], - actionVariables: { - context: [ +export const getMonitorSummary = (monitorInfo: Ping) => { + return { + monitorUrl: monitorInfo.url?.full, + monitorId: monitorInfo.monitor?.id, + monitorName: monitorInfo.monitor?.name ?? monitorInfo.monitor?.id, + monitorType: monitorInfo.monitor?.type, + latestErrorMessage: monitorInfo.error?.message, + observerLocation: monitorInfo.observer?.geo?.name ?? UNNAMED_LOCATION, + observerHostname: monitorInfo.agent?.name, + }; +}; + +const generateMessageForOlderVersions = (fields: Record) => { + const messageTemplate = MonitorStatusTranslations.defaultActionMessage; + + // Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from + // {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}} + + return Mustache.render(messageTemplate, { state: { ...fields } }); +}; + +export const getStatusMessage = ( + downMonInfo?: Ping, + availMonInfo?: GetMonitorAvailabilityResult, + availability?: GetMonitorAvailabilityParams +) => { + let statusMessage = ''; + if (downMonInfo) { + statusMessage = DOWN_LABEL; + } + let availabilityMessage = ''; + + if (availMonInfo) { + availabilityMessage = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.availabilityMessage', { - name: 'message', - description: i18n.translate( - 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description', - { - defaultMessage: 'A generated message summarizing the currently down monitors', - } - ), - }, + defaultMessage: + 'below threshold with {availabilityRatio}% availability expected is {expectedAvailability}%', + values: { + availabilityRatio: (availMonInfo.availabilityRatio! * 100).toFixed(2), + expectedAvailability: availability?.threshold, + }, + } + ); + } + if (availMonInfo && downMonInfo) { + return i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.downAndAvailabilityMessage', { - name: 'downMonitorsWithGeo', - description: i18n.translate( - 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description', - { - defaultMessage: - 'A generated summary that shows some or all of the monitors detected as "down" by the alert', - } + defaultMessage: '{statusMessage} and also {availabilityMessage}', + values: { + statusMessage, + availabilityMessage, + }, + } + ); + } + return statusMessage + availabilityMessage; +}; + +export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => + uptimeAlertWrapper({ + id: 'xpack.uptime.alerts.monitorStatus', + name: i18n.translate('xpack.uptime.alerts.monitorStatus', { + defaultMessage: 'Uptime monitor status', + }), + validate: { + params: schema.object({ + availability: schema.maybe( + schema.object({ + range: schema.number(), + rangeUnit: schema.string(), + threshold: schema.string(), + }) + ), + filters: schema.maybe( + schema.oneOf([ + // deprecated + schema.object({ + 'monitor.type': schema.maybe(schema.arrayOf(schema.string())), + 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())), + tags: schema.maybe(schema.arrayOf(schema.string())), + 'url.port': schema.maybe(schema.arrayOf(schema.string())), + }), + schema.string(), + ]) + ), + // deprecated + locations: schema.maybe(schema.arrayOf(schema.string())), + numTimes: schema.number(), + search: schema.maybe(schema.string()), + shouldCheckStatus: schema.boolean(), + shouldCheckAvailability: schema.boolean(), + timerangeCount: schema.maybe(schema.number()), + timerangeUnit: schema.maybe(schema.string()), + // deprecated + timerange: schema.maybe( + schema.object({ + from: schema.string(), + to: schema.string(), + }) ), + version: schema.maybe(schema.number()), + }), + }, + defaultActionGroupId: MONITOR_STATUS.id, + actionGroups: [ + { + id: MONITOR_STATUS.id, + name: MONITOR_STATUS.name, }, ], - state: [...commonStateTranslations], - }, - producer: 'uptime', - async executor(options: AlertExecutorOptions) { - const { params: rawParams } = options; - const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( - options.services.savedObjectsClient - ); - const atomicDecoded = AtomicStatusCheckParamsType.decode(rawParams); - const availabilityDecoded = MonitorAvailabilityType.decode(rawParams); - const decoded = StatusCheckParamsType.decode(rawParams); - let filterString: string = ''; - let params: StatusCheckParams; - if (isRight(atomicDecoded)) { - const { filters, search, numTimes, timerangeCount, timerangeUnit } = atomicDecoded.right; - const timerange = { from: `now-${String(timerangeCount) + timerangeUnit}`, to: 'now' }; - filterString = await formatFilterString(libs, dynamicSettings, options, filters, search); - params = { - timerange, + actionVariables: { + context: [ + { + name: 'message', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description', + { + defaultMessage: 'A generated message summarizing the currently down monitors', + } + ), + }, + { + name: 'downMonitorsWithGeo', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description', + { + defaultMessage: + 'A generated summary that shows some or all of the monitors detected as "down" by the alert', + } + ), + }, + ], + state: [...commonMonitorStateI18, ...commonStateTranslations], + }, + async executor( + { params: rawParams, state, services: { alertInstanceFactory } }, + callES, + dynamicSettings + ) { + const { + filters, + search, numTimes, - locations: [], - filters: filterString, - }; - } else if (isRight(decoded)) { - params = decoded.right; - } else if (!isRight(availabilityDecoded)) { - ThrowReporter.report(decoded); - return { - error: 'Alert param types do not conform to required shape.', + timerangeCount, + timerangeUnit, + availability, + shouldCheckAvailability, + shouldCheckStatus, + timerange: oldVersionTimeRange, + } = rawParams; + + const timerange = oldVersionTimeRange || { + from: `now-${String(timerangeCount) + timerangeUnit}`, + to: 'now', }; - } - let availabilityResults: GetMonitorAvailabilityResult[] = []; - if ( - isRight(availabilityDecoded) && - availabilityDecoded.right.shouldCheckAvailability === true - ) { - const { filters, search } = availabilityDecoded.right; - if (filterString === '' && (filters || search)) { - filterString = await formatFilterString(libs, dynamicSettings, options, filters, search); + const filterString = await formatFilterString(libs, dynamicSettings, callES, filters, search); + + let availabilityResults: GetMonitorAvailabilityResult[] = []; + if (shouldCheckAvailability) { + availabilityResults = await libs.requests.getMonitorAvailability({ + callES, + dynamicSettings, + ...availability, + filters: JSON.stringify(filterString) || undefined, + }); } - availabilityResults = await libs.requests.getMonitorAvailability({ - callES: options.services.callCluster, - dynamicSettings, - ...availabilityDecoded.right.availability, - filters: filterString || undefined, - }); - } + let downMonitorsByLocation: GetMonitorStatusResult[] = []; - /* This is called `monitorsByLocation` but it's really - * monitors by location by status. The query we run to generate this - * filters on the status field, so effectively there should be one and only one - * status represented in the result set. */ - let monitorsByLocation: GetMonitorStatusResult[] = []; - - // old alert versions are missing this field so it must default to true - const verifiedParams = StatusCheckParamsType.decode(params!); - if (isRight(verifiedParams) && (verifiedParams.right?.shouldCheckStatus ?? true)) { - monitorsByLocation = await libs.requests.getMonitorStatus({ - callES: options.services.callCluster, - dynamicSettings, - ...verifiedParams.right, - }); - } + // if oldVersionTimeRange present means it's 7.7 format and + // after that shouldCheckStatus should be explicitly false + if (!(!oldVersionTimeRange && shouldCheckStatus === false)) { + downMonitorsByLocation = await libs.requests.getMonitorStatus({ + callES, + dynamicSettings, + timerange, + numTimes, + locations: [], + filters: filterString, + }); + } - // if no monitors are down for our query, we don't need to trigger an alert - if (monitorsByLocation.length || availabilityResults.length) { - const uniqueIds = uniqueMonitorIds(monitorsByLocation); - const alertInstance = options.services.alertInstanceFactory(MONITOR_STATUS.id); - alertInstance.replaceState({ - ...options.state, - monitors: monitorsByLocation, - ...updateState(options.state, true), - }); - alertInstance.scheduleActions(MONITOR_STATUS.id, { - message: contextMessage( - Array.from(uniqueIds.keys()), - DEFAULT_MAX_MESSAGE_ROWS, - availabilityResults, - isRight(availabilityDecoded) ? availabilityDecoded.right.availability.threshold : '100', - isRight(availabilityDecoded) && availabilityDecoded.right.shouldCheckAvailability, - rawParams?.shouldCheckStatus ?? false - ), - downMonitorsWithGeo: fullListByIdAndLocation(monitorsByLocation), + const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults); + + mergedIdsByLoc.forEach((monIdByLoc) => { + const alertInstance = alertInstanceFactory(MONITOR_STATUS.id + monIdByLoc); + + const availMonInfo = availabilityResults.find( + ({ monitorId, location }) => monitorId + location === monIdByLoc + ); + + const downMonInfo = downMonitorsByLocation.find( + ({ monitorId, location }) => monitorId + location === monIdByLoc + )?.monitorInfo; + + const monitorSummary = getMonitorSummary(downMonInfo || availMonInfo?.monitorInfo!); + const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability); + + alertInstance.replaceState({ + ...updateState(state, true), + ...monitorSummary, + statusMessage, + }); + + alertInstance.scheduleActions(MONITOR_STATUS.id, { + message: generateMessageForOlderVersions({ ...monitorSummary, statusMessage }), + }); }); - } - return updateState(options.state, monitorsByLocation.length > 0); - }, -}); + return updateState(state, downMonitorsByLocation.length > 0); + }, + }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/translations.ts b/x-pack/plugins/uptime/server/lib/alerts/translations.ts index 50eedcd4fa69e4..8e5c0e76ad589f 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/translations.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/translations.ts @@ -6,6 +6,79 @@ import { i18n } from '@kbn/i18n'; +export const commonMonitorStateI18 = [ + { + name: 'monitorName', + description: i18n.translate('xpack.uptime.alerts.monitorStatus.actionVariables.state.monitor', { + defaultMessage: 'A human friendly rendering of name or ID, preferring name (e.g. My Monitor)', + }), + }, + { + name: 'monitorId', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorId', + { + defaultMessage: 'ID of the monitor.', + } + ), + }, + { + name: 'monitorUrl', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorUrl', + { + defaultMessage: 'URL of the monitor.', + } + ), + }, + { + name: 'monitorType', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.state.monitorType', + { + defaultMessage: 'Type (e.g. HTTP/TCP) of the monitor.', + } + ), + }, + { + name: 'statusMessage', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.state.statusMessage', + { + defaultMessage: + 'Status message e.g down or is below availability threshold in case of availability check or both.', + } + ), + }, + { + name: 'latestErrorMessage', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastErrorMessage', + { + defaultMessage: 'Monitor latest error message', + } + ), + }, + { + name: 'observerLocation', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.state.observerLocation', + { + defaultMessage: 'Observer location from which heartbeat check is performed.', + } + ), + }, + { + name: 'observerHostname', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.state.observerHostname', + { + defaultMessage: 'Observer hostname from which heartbeat check is performed.', + } + ), + }, +]; + export const commonStateTranslations = [ { name: 'firstCheckedAt', @@ -238,3 +311,7 @@ export const durationAnomalyTranslations = { }, ], }; + +export const DOWN_LABEL = i18n.translate('xpack.uptime.alerts.monitorStatus.actionVariables.down', { + defaultMessage: 'down', +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index 172930bc3dd3bd..0a80b36046860d 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertType } from '../../../../alerts/server'; import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters'; import { UMServerLibs } from '../lib'; +import { AlertType } from '../../../../alerts/server'; export type UptimeAlertTypeFactory = ( server: UptimeCoreSetup, diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts new file mode 100644 index 00000000000000..5963b371f844f3 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ILegacyScopedClusterClient } from 'kibana/server'; +import { AlertExecutorOptions, AlertType, State } from '../../../../alerts/server'; +import { savedObjectsAdapter } from '../saved_objects'; +import { DynamicSettings } from '../../../common/runtime_types'; + +export interface UptimeAlertType extends Omit { + executor: ( + options: AlertExecutorOptions, + callES: ILegacyScopedClusterClient['callAsCurrentUser'], + dynamicSettings: DynamicSettings + ) => Promise; +} + +export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({ + ...uptimeAlert, + producer: 'uptime', + executor: async (options: AlertExecutorOptions) => { + const { + services: { callCluster: callES }, + } = options; + + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + options.services.savedObjectsClient + ); + + return uptimeAlert.executor(options, callES, dynamicSettings); + }, +}); diff --git a/x-pack/plugins/uptime/server/lib/compose/kibana.ts b/x-pack/plugins/uptime/server/lib/compose/kibana.ts index edda5cb283323f..783a77b9e53771 100644 --- a/x-pack/plugins/uptime/server/lib/compose/kibana.ts +++ b/x-pack/plugins/uptime/server/lib/compose/kibana.ts @@ -5,23 +5,17 @@ */ import { UMKibanaBackendFrameworkAdapter } from '../adapters/framework'; -import * as requests from '../requests'; +import { requests } from '../requests'; import { licenseCheck } from '../domains'; -import { UMDomainLibs, UMServerLibs } from '../lib'; +import { UMServerLibs } from '../lib'; import { UptimeCoreSetup } from '../adapters/framework'; export function compose(server: UptimeCoreSetup): UMServerLibs { const framework = new UMKibanaBackendFrameworkAdapter(server); - const domainLibs: UMDomainLibs = { - requests: { - ...requests, - }, - license: licenseCheck, - }; - return { framework, - ...domainLibs, + requests, + license: licenseCheck, }; } diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts index e014aa985a82d1..f0222de02697db 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts @@ -12,16 +12,9 @@ import { } from '../get_monitor_availability'; import { setupMockEsCompositeQuery } from './helper'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { GetMonitorAvailabilityParams } from '../../../../common/runtime_types'; +import { GetMonitorAvailabilityParams, makePing, Ping } from '../../../../common/runtime_types'; interface AvailabilityTopHit { - _source: { - monitor: { - name: string; - }; - url: { - full: string; - }; - }; + _source: Ping; } interface AvailabilityDoc { @@ -46,11 +39,10 @@ interface AvailabilityDoc { const genBucketItem = ({ monitorId, location, - name, - url, up, down, availabilityRatio, + monitorInfo, }: GetMonitorAvailabilityResult): AvailabilityDoc => ({ key: { monitorId, @@ -61,14 +53,7 @@ const genBucketItem = ({ hits: { hits: [ { - _source: { - monitor: { - name, - }, - url: { - full: url, - }, - }, + _source: monitorInfo, }, ], }, @@ -148,10 +133,6 @@ describe('monitor availability', () => { }, "fields": Object { "top_hits": Object { - "_source": Array [ - "monitor.name", - "url.full", - ], "size": 1, "sort": Array [ Object { @@ -271,29 +252,26 @@ describe('monitor availability', () => { { monitorId: 'foo', location: 'harrisburg', - name: 'Foo', - url: 'http://foo.com', up: 456, down: 234, availabilityRatio: 0.660869565217391, + monitorInfo: makePing({}), }, { monitorId: 'foo', location: 'faribanks', - name: 'Foo', - url: 'http://foo.com', up: 450, down: 240, availabilityRatio: 0.652173913043478, + monitorInfo: makePing({}), }, { monitorId: 'bar', location: 'fairbanks', - name: 'Bar', - url: 'http://bar.com', up: 468, down: 212, availabilityRatio: 0.688235294117647, + monitorInfo: makePing({}), }, ], }, @@ -327,10 +305,6 @@ describe('monitor availability', () => { }, "fields": Object { "top_hits": Object { - "_source": Array [ - "monitor.name", - "url.full", - ], "size": 1, "sort": Array [ Object { @@ -417,27 +391,63 @@ describe('monitor availability', () => { "down": 234, "location": "harrisburg", "monitorId": "foo", - "name": "Foo", + "monitorInfo": Object { + "docId": "myDocId", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "myId", + "ip": "127.0.0.1", + "name": undefined, + "status": "up", + "type": "myType", + }, + "timestamp": "2020-07-07T01:14:08Z", + }, "up": 456, - "url": "http://foo.com", }, Object { "availabilityRatio": 0.652173913043478, "down": 240, "location": "faribanks", "monitorId": "foo", - "name": "Foo", + "monitorInfo": Object { + "docId": "myDocId", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "myId", + "ip": "127.0.0.1", + "name": undefined, + "status": "up", + "type": "myType", + }, + "timestamp": "2020-07-07T01:14:08Z", + }, "up": 450, - "url": "http://foo.com", }, Object { "availabilityRatio": 0.688235294117647, "down": 212, "location": "fairbanks", "monitorId": "bar", - "name": "Bar", + "monitorInfo": Object { + "docId": "myDocId", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "myId", + "ip": "127.0.0.1", + "name": undefined, + "status": "up", + "type": "myType", + }, + "timestamp": "2020-07-07T01:14:08Z", + }, "up": 468, - "url": "http://bar.com", }, ] `); @@ -459,20 +469,18 @@ describe('monitor availability', () => { { monitorId: 'foo', location: 'harrisburg', - name: 'Foo', - url: 'http://foo.com', up: 243, down: 11, availabilityRatio: 0.956692913385827, + monitorInfo: makePing({}), }, { monitorId: 'foo', location: 'fairbanks', - name: 'Foo', - url: 'http://foo.com', up: 251, down: 13, availabilityRatio: 0.950757575757576, + monitorInfo: makePing({}), }, ], }, @@ -481,20 +489,18 @@ describe('monitor availability', () => { { monitorId: 'baz', location: 'harrisburg', - name: 'Baz', - url: 'http://baz.com', up: 341, down: 3, availabilityRatio: 0.991279069767442, + monitorInfo: makePing({}), }, { monitorId: 'baz', location: 'fairbanks', - name: 'Baz', - url: 'http://baz.com', up: 365, down: 5, availabilityRatio: 0.986486486486486, + monitorInfo: makePing({}), }, ], }, @@ -515,36 +521,84 @@ describe('monitor availability', () => { "down": 11, "location": "harrisburg", "monitorId": "foo", - "name": "Foo", + "monitorInfo": Object { + "docId": "myDocId", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "myId", + "ip": "127.0.0.1", + "name": undefined, + "status": "up", + "type": "myType", + }, + "timestamp": "2020-07-07T01:14:08Z", + }, "up": 243, - "url": "http://foo.com", }, Object { "availabilityRatio": 0.950757575757576, "down": 13, "location": "fairbanks", "monitorId": "foo", - "name": "Foo", + "monitorInfo": Object { + "docId": "myDocId", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "myId", + "ip": "127.0.0.1", + "name": undefined, + "status": "up", + "type": "myType", + }, + "timestamp": "2020-07-07T01:14:08Z", + }, "up": 251, - "url": "http://foo.com", }, Object { "availabilityRatio": 0.991279069767442, "down": 3, "location": "harrisburg", "monitorId": "baz", - "name": "Baz", + "monitorInfo": Object { + "docId": "myDocId", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "myId", + "ip": "127.0.0.1", + "name": undefined, + "status": "up", + "type": "myType", + }, + "timestamp": "2020-07-07T01:14:08Z", + }, "up": 341, - "url": "http://baz.com", }, Object { "availabilityRatio": 0.986486486486486, "down": 5, "location": "fairbanks", "monitorId": "baz", - "name": "Baz", + "monitorInfo": Object { + "docId": "myDocId", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "myId", + "ip": "127.0.0.1", + "name": undefined, + "status": "up", + "type": "myType", + }, + "timestamp": "2020-07-07T01:14:08Z", + }, "up": 365, - "url": "http://baz.com", }, ] `); @@ -565,10 +619,6 @@ describe('monitor availability', () => { }, "fields": Object { "top_hits": Object { - "_source": Array [ - "monitor.name", - "url.full", - ], "size": 1, "sort": Array [ Object { @@ -663,10 +713,6 @@ describe('monitor availability', () => { }, "fields": Object { "top_hits": Object { - "_source": Array [ - "monitor.name", - "url.full", - ], "size": 1, "sort": Array [ Object { @@ -833,18 +879,30 @@ describe('monitor availability', () => { "down": 2450, "location": "fairbanks", "monitorId": "test-node-service", - "name": "Test Node Service", + "monitorInfo": Object { + "monitor": Object { + "name": "Test Node Service", + }, + "url": Object { + "full": "http://localhost:12349", + }, + }, "up": 821, - "url": "http://localhost:12349", }, Object { "availabilityRatio": 0.5804076040417879, "down": 2450, "location": "harrisburg", "monitorId": "test-node-service", - "name": "Test Node Service", + "monitorInfo": Object { + "monitor": Object { + "name": "Test Node Service", + }, + "url": Object { + "full": "http://localhost:12349", + }, + }, "up": 3389, - "url": "http://localhost:12349", }, ] `); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index f12f1527fb56cb..7dba71a8126e2c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -9,14 +9,14 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; import { setupMockEsCompositeQuery } from './helper'; export interface BucketItemCriteria { - monitor_id: string; + monitorId: string; status: string; location: string; doc_count: number; } interface BucketKey { - monitor_id: string; + monitorId: string; status: string; location: string; } @@ -27,19 +27,17 @@ interface BucketItem { } const genBucketItem = ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - monitor_id, + monitorId, status, location, - // eslint-disable-next-line @typescript-eslint/naming-convention - doc_count, + doc_count: count, }: BucketItemCriteria): BucketItem => ({ key: { - monitor_id, + monitorId, status, location, }, - doc_count, + doc_count: count, }); describe('getMonitorStatus', () => { @@ -48,37 +46,37 @@ describe('getMonitorStatus', () => { [], genBucketItem ); - const exampleFilter = `{ - "bool": { - "should": [ + const exampleFilter = { + bool: { + should: [ { - "bool": { - "should": [ + bool: { + should: [ { - "match_phrase": { - "monitor.id": "apm-dev" - } - } + match_phrase: { + 'monitor.id': 'apm-dev', + }, + }, ], - "minimum_should_match": 1 - } + minimum_should_match: 1, + }, }, { - "bool": { - "should": [ + bool: { + should: [ { - "match_phrase": { - "monitor.id": "auto-http-0X8D6082B94BBE3B8A" - } - } + match_phrase: { + 'monitor.id': 'auto-http-0X8D6082B94BBE3B8A', + }, + }, ], - "minimum_should_match": 1 - } - } + minimum_should_match: 1, + }, + }, ], - "minimum_should_match": 1 - } - }`; + minimum_should_match: 1, + }, + }; await getMonitorStatus({ callES, dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, @@ -98,11 +96,18 @@ describe('getMonitorStatus', () => { "body": Object { "aggs": Object { "monitors": Object { + "aggs": Object { + "fields": Object { + "top_hits": Object { + "size": 1, + }, + }, + }, "composite": Object { "size": 2000, "sources": Array [ Object { - "monitor_id": Object { + "monitorId": Object { "terms": Object { "field": "monitor.id", }, @@ -203,11 +208,18 @@ describe('getMonitorStatus', () => { "body": Object { "aggs": Object { "monitors": Object { + "aggs": Object { + "fields": Object { + "top_hits": Object { + "size": 1, + }, + }, + }, "composite": Object { "size": 2000, "sources": Array [ Object { - "monitor_id": Object { + "monitorId": Object { "terms": Object { "field": "monitor.id", }, @@ -280,19 +292,19 @@ describe('getMonitorStatus', () => { { bucketCriteria: [ { - monitor_id: 'foo', + monitorId: 'foo', status: 'down', location: 'fairbanks', doc_count: 43, }, { - monitor_id: 'bar', + monitorId: 'bar', status: 'down', location: 'harrisburg', doc_count: 53, }, { - monitor_id: 'foo', + monitorId: 'foo', status: 'down', location: 'harrisburg', doc_count: 44, @@ -324,11 +336,18 @@ describe('getMonitorStatus', () => { "body": Object { "aggs": Object { "monitors": Object { + "aggs": Object { + "fields": Object { + "top_hits": Object { + "size": 1, + }, + }, + }, "composite": Object { "size": 2000, "sources": Array [ Object { - "monitor_id": Object { + "monitorId": Object { "terms": Object { "field": "monitor.id", }, @@ -377,25 +396,29 @@ describe('getMonitorStatus', () => { "index": "heartbeat-8*", } `); + expect(result.length).toBe(3); expect(result).toMatchInlineSnapshot(` Array [ Object { "count": 43, "location": "fairbanks", - "monitor_id": "foo", + "monitorId": "foo", + "monitorInfo": undefined, "status": "down", }, Object { "count": 53, "location": "harrisburg", - "monitor_id": "bar", + "monitorId": "bar", + "monitorInfo": undefined, "status": "down", }, Object { "count": 44, "location": "harrisburg", - "monitor_id": "foo", + "monitorId": "foo", + "monitorInfo": undefined, "status": "down", }, ] @@ -406,25 +429,25 @@ describe('getMonitorStatus', () => { const criteria = [ { after_key: { - monitor_id: 'foo', + monitorId: 'foo', location: 'harrisburg', status: 'down', }, bucketCriteria: [ { - monitor_id: 'foo', + monitorId: 'foo', status: 'down', location: 'fairbanks', doc_count: 43, }, { - monitor_id: 'bar', + monitorId: 'bar', status: 'down', location: 'harrisburg', doc_count: 53, }, { - monitor_id: 'foo', + monitorId: 'foo', status: 'down', location: 'harrisburg', doc_count: 44, @@ -433,25 +456,25 @@ describe('getMonitorStatus', () => { }, { after_key: { - monitor_id: 'bar', + monitorId: 'bar', status: 'down', location: 'fairbanks', }, bucketCriteria: [ { - monitor_id: 'sna', + monitorId: 'sna', status: 'down', location: 'fairbanks', doc_count: 21, }, { - monitor_id: 'fu', + monitorId: 'fu', status: 'down', location: 'fairbanks', doc_count: 21, }, { - monitor_id: 'bar', + monitorId: 'bar', status: 'down', location: 'fairbanks', doc_count: 45, @@ -461,13 +484,13 @@ describe('getMonitorStatus', () => { { bucketCriteria: [ { - monitor_id: 'sna', + monitorId: 'sna', status: 'down', location: 'harrisburg', doc_count: 21, }, { - monitor_id: 'fu', + monitorId: 'fu', status: 'down', location: 'harrisburg', doc_count: 21, @@ -489,54 +512,63 @@ describe('getMonitorStatus', () => { to: 'now-1m', }, }); + expect(result.length).toBe(8); expect(result).toMatchInlineSnapshot(` Array [ Object { "count": 43, "location": "fairbanks", - "monitor_id": "foo", + "monitorId": "foo", + "monitorInfo": undefined, "status": "down", }, Object { "count": 53, "location": "harrisburg", - "monitor_id": "bar", + "monitorId": "bar", + "monitorInfo": undefined, "status": "down", }, Object { "count": 44, "location": "harrisburg", - "monitor_id": "foo", + "monitorId": "foo", + "monitorInfo": undefined, "status": "down", }, Object { "count": 21, "location": "fairbanks", - "monitor_id": "sna", + "monitorId": "sna", + "monitorInfo": undefined, "status": "down", }, Object { "count": 21, "location": "fairbanks", - "monitor_id": "fu", + "monitorId": "fu", + "monitorInfo": undefined, "status": "down", }, Object { "count": 45, "location": "fairbanks", - "monitor_id": "bar", + "monitorId": "bar", + "monitorInfo": undefined, "status": "down", }, Object { "count": 21, "location": "harrisburg", - "monitor_id": "sna", + "monitorId": "sna", + "monitorInfo": undefined, "status": "down", }, Object { "count": 21, "location": "harrisburg", - "monitor_id": "fu", + "monitorId": "fu", + "monitorInfo": undefined, "status": "down", }, ] diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts index 798cefc404e1f9..0801fc5769363e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { GetMonitorAvailabilityParams } from '../../../common/runtime_types'; +import { GetMonitorAvailabilityParams, Ping } from '../../../common/runtime_types'; export interface AvailabilityKey { monitorId: string; @@ -14,20 +14,18 @@ export interface AvailabilityKey { export interface GetMonitorAvailabilityResult { monitorId: string; - location: string; - name: string; - url: string; up: number; down: number; + location: string; availabilityRatio: number | null; + monitorInfo: Ping; } export const formatBuckets = async (buckets: any[]): Promise => // eslint-disable-next-line @typescript-eslint/naming-convention buckets.map(({ key, fields, up_sum, down_sum, ratio }: any) => ({ ...key, - name: fields?.hits?.hits?.[0]?._source?.monitor.name, - url: fields?.hits?.hits?.[0]?._source?.url.full, + monitorInfo: fields?.hits?.hits?.[0]?._source, up: up_sum.value, down: down_sum.value, availabilityRatio: ratio.value, @@ -94,7 +92,6 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< fields: { top_hits: { size: 1, - _source: ['monitor.name', 'url.full'], sort: [ { '@timestamp': { @@ -143,8 +140,8 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< }; if (filters) { - const parsedFilters = JSON.parse(filters); - esParams.body.query.bool = { ...esParams.body.query.bool, ...parsedFilters.bool }; + const parsedFilter = JSON.parse(filters); + esParams.body.query.bool = { ...esParams.body.query.bool, ...parsedFilter.bool }; } if (afterKey) { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index a52bbfc8f24424..0788880994200e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -4,20 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { JsonObject } from 'src/plugins/kibana_utils/public'; import { UMElasticsearchQueryFn } from '../adapters'; +import { Ping } from '../../../common/runtime_types/ping'; export interface GetMonitorStatusParams { - filters?: string; + filters?: JsonObject; locations: string[]; numTimes: number; timerange: { from: string; to: string }; } export interface GetMonitorStatusResult { - monitor_id: string; + monitorId: string; status: string; location: string; count: number; + monitorInfo: Ping; } interface MonitorStatusKey { @@ -26,15 +29,6 @@ interface MonitorStatusKey { location: string; } -const formatBuckets = async ( - buckets: any[], - numTimes: number -): Promise => { - return buckets - .filter((monitor: any) => monitor?.doc_count > numTimes) - .map(({ key, doc_count: count }: any) => ({ ...key, count })); -}; - const getLocationClause = (locations: string[]) => ({ bool: { should: [ @@ -51,10 +45,10 @@ export const getMonitorStatus: UMElasticsearchQueryFn< GetMonitorStatusParams, GetMonitorStatusResult[] > = async ({ callES, dynamicSettings, filters, locations, numTimes, timerange: { from, to } }) => { - const queryResults: Array> = []; let afterKey: MonitorStatusKey | undefined; const STATUS = 'down'; + let monitors: any = []; do { // today this value is hardcoded. In the future we may support // multiple status types for this alert, and this will become a parameter @@ -87,7 +81,7 @@ export const getMonitorStatus: UMElasticsearchQueryFn< size: 2000, sources: [ { - monitor_id: { + monitorId: { terms: { field: 'monitor.id', }, @@ -110,18 +104,20 @@ export const getMonitorStatus: UMElasticsearchQueryFn< }, ], }, + aggs: { + fields: { + top_hits: { + size: 1, + }, + }, + }, }, }, }, }; - /** - * `filters` are an unparsed JSON string. We parse them and append the bool fields of the query - * to the bool of the parsed filters. - */ - if (filters) { - const parsedFilters = JSON.parse(filters); - esParams.body.query.bool = Object.assign({}, esParams.body.query.bool, parsedFilters.bool); + if (filters?.bool) { + esParams.body.query.bool = Object.assign({}, esParams.body.query.bool, filters.bool); } /** @@ -142,8 +138,14 @@ export const getMonitorStatus: UMElasticsearchQueryFn< const result = await callES('search', esParams); afterKey = result?.aggregations?.monitors?.after_key; - queryResults.push(formatBuckets(result?.aggregations?.monitors?.buckets || [], numTimes)); + monitors = monitors.concat(result?.aggregations?.monitors?.buckets || []); } while (afterKey !== undefined); - return (await Promise.all(queryResults)).reduce((acc, cur) => acc.concat(cur), []); + return monitors + .filter((monitor: any) => monitor?.doc_count > numTimes) + .map(({ key, doc_count: count, fields }: any) => ({ + ...key, + count, + monitorInfo: fields?.hits?.hits?.[0]?._source, + })); }; diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index 415b3d2f4b4a17..8fa4561268e8f7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -4,19 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getCerts } from './get_certs'; -export { getFilterBar, GetFilterBarParams } from './get_filter_bar'; -export { getUptimeIndexPattern as getIndexPattern } from './get_index_pattern'; -export { getLatestMonitor, GetLatestMonitorParams } from './get_latest_monitor'; -export { getMonitorAvailability } from './get_monitor_availability'; -export { getMonitorDurationChart, GetMonitorChartsParams } from './get_monitor_duration'; -export { getMonitorDetails, GetMonitorDetailsParams } from './get_monitor_details'; -export { getMonitorLocations, GetMonitorLocationsParams } from './get_monitor_locations'; -export { getMonitorStates, GetMonitorStatesParams } from './get_monitor_states'; -export { getMonitorStatus, GetMonitorStatusParams } from './get_monitor_status'; -export * from './get_monitor_status'; -export { getPings } from './get_pings'; -export { getPingHistogram, GetPingHistogramParams } from './get_ping_histogram'; -export { UptimeRequests } from './uptime_requests'; -export { getSnapshotCount, GetSnapshotCountParams } from './get_snapshot_counts'; -export { getIndexStatus } from './get_index_status'; +import { getCerts } from './get_certs'; +import { getFilterBar } from './get_filter_bar'; +import { getUptimeIndexPattern as getIndexPattern } from './get_index_pattern'; +import { getLatestMonitor } from './get_latest_monitor'; +import { getMonitorAvailability } from './get_monitor_availability'; +import { getMonitorDurationChart } from './get_monitor_duration'; +import { getMonitorDetails } from './get_monitor_details'; +import { getMonitorLocations } from './get_monitor_locations'; +import { getMonitorStates } from './get_monitor_states'; +import { getMonitorStatus } from './get_monitor_status'; +import { getPings } from './get_pings'; +import { getPingHistogram } from './get_ping_histogram'; +import { getSnapshotCount } from './get_snapshot_counts'; +import { getIndexStatus } from './get_index_status'; + +export const requests = { + getCerts, + getFilterBar, + getIndexPattern, + getLatestMonitor, + getMonitorAvailability, + getMonitorDurationChart, + getMonitorDetails, + getMonitorLocations, + getMonitorStates, + getMonitorStatus, + getPings, + getPingHistogram, + getSnapshotCount, + getIndexStatus, +}; + +export type UptimeRequests = typeof requests; diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts deleted file mode 100644 index 2a9420a2755709..00000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UMElasticsearchQueryFn } from '../adapters'; -import { - OverviewFilters, - GetMonitorAvailabilityParams, - MonitorDetails, - MonitorLocations, - Snapshot, - StatesIndexStatus, - HistogramResult, - Ping, - PingsResponse, - GetCertsParams, - GetPingsParams, - CertResult, - MonitorSummariesResult, -} from '../../../common/runtime_types'; -import { MonitorDurationResult } from '../../../common/types'; - -import { - GetFilterBarParams, - GetLatestMonitorParams, - GetMonitorChartsParams, - GetMonitorDetailsParams, - GetMonitorLocationsParams, - GetMonitorStatesParams, - GetPingHistogramParams, - GetMonitorStatusParams, - GetMonitorStatusResult, -} from '.'; -import { GetSnapshotCountParams } from './get_snapshot_counts'; -import { IIndexPattern } from '../../../../../../src/plugins/data/server'; -import { GetMonitorAvailabilityResult } from './get_monitor_availability'; - -type ESQ = UMElasticsearchQueryFn; - -export interface UptimeRequests { - getCerts: ESQ; - getFilterBar: ESQ; - getIndexPattern: ESQ<{}, IIndexPattern | undefined>; - getLatestMonitor: ESQ; - getMonitorAvailability: ESQ; - getMonitorDurationChart: ESQ; - getMonitorDetails: ESQ; - getMonitorLocations: ESQ; - getMonitorStates: ESQ; - getMonitorStatus: ESQ; - getPings: ESQ; - getPingHistogram: ESQ; - getSnapshotCount: ESQ; - getIndexStatus: ESQ<{}, StatesIndexStatus>; -} From bcf8719824bbc47e482582a13c84e12a39706e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 14 Aug 2020 08:55:39 +0100 Subject: [PATCH 15/46] Adding API test for custom link transaction example (#74238) * Adding api test for custom link transaction example * expecting specific fields * expecting specific fields Co-authored-by: Elastic Machine --- .../basic/tests/settings/custom_link.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts index 9465708db2fba6..2acc6522bf4797 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts @@ -12,6 +12,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { const supertestRead = getService('supertestAsApmReadUser'); const supertestWrite = getService('supertestAsApmWriteUser'); const log = getService('log'); + const esArchiver = getService('esArchiver'); function searchCustomLinks(filters?: any) { const path = URL.format({ @@ -139,5 +140,18 @@ export default function customLinksTests({ getService }: FtrProviderContext) { expect(status).to.equal(200); expect(body).to.eql([]); }); + + describe('transaction', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('fetches a transaction sample', async () => { + const response = await supertestRead.get( + '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' + ); + expect(response.status).to.be(200); + expect(response.body.service.name).to.eql('opbeans-java'); + }); + }); }); } From 7bd014abb387a4b6d896abf992c96f2ab98fad65 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 14 Aug 2020 12:07:04 +0200 Subject: [PATCH 16/46] [UiActions] pass trigger into action execution context (#74363) Co-authored-by: Elastic Machine --- .../public/actions/actions.tsx | 9 ++- examples/ui_actions_explorer/public/app.tsx | 6 +- .../lib/actions/apply_filter_action.test.ts | 4 + .../public/lib/panel/embeddable_panel.tsx | 7 +- .../add_panel/add_panel_action.test.tsx | 9 ++- .../add_panel/add_panel_action.ts | 10 ++- .../lib/panel/panel_header/panel_header.tsx | 19 +++-- .../test_samples/actions/say_hello_action.tsx | 4 +- .../public/tests/apply_filter_action.test.ts | 3 +- .../ui_actions/public/actions/action.test.ts | 13 +++- .../ui_actions/public/actions/action.ts | 76 +++++++++++++++---- .../build_eui_context_menu_panels.tsx | 53 ++++++++++--- src/plugins/ui_actions/public/index.ts | 7 +- .../service/ui_actions_execution_service.ts | 17 +++-- .../public/service/ui_actions_service.ts | 11 ++- .../tests/execute_trigger_actions.test.ts | 24 +++++- .../public/triggers/default_trigger.ts | 27 +++++++ .../ui_actions/public/triggers/index.ts | 1 + src/plugins/ui_actions/public/types.ts | 9 ++- .../dashboard_to_url_drilldown/index.tsx | 11 ++- .../panel_actions/get_csv_panel_action.tsx | 7 +- .../public/custom_time_range_action.tsx | 7 +- .../public/drilldowns/drilldown_definition.ts | 11 ++- 23 files changed, 277 insertions(+), 68 deletions(-) create mode 100644 src/plugins/ui_actions/public/triggers/default_trigger.ts diff --git a/examples/ui_actions_explorer/public/actions/actions.tsx b/examples/ui_actions_explorer/public/actions/actions.tsx index 6d83362e998bc6..777bcd9c181192 100644 --- a/examples/ui_actions_explorer/public/actions/actions.tsx +++ b/examples/ui_actions_explorer/public/actions/actions.tsx @@ -21,7 +21,11 @@ import { OverlayStart } from 'kibana/public'; import { EuiFieldText, EuiModalBody, EuiButton } from '@elastic/eui'; import { useState } from 'react'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { createAction, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { + ActionExecutionContext, + createAction, + UiActionsStart, +} from '../../../../src/plugins/ui_actions/public'; export const USER_TRIGGER = 'USER_TRIGGER'; export const COUNTRY_TRIGGER = 'COUNTRY_TRIGGER'; @@ -37,7 +41,8 @@ export const ACTION_SHOWCASE_PLUGGABILITY = 'ACTION_SHOWCASE_PLUGGABILITY'; export const showcasePluggability = createAction({ type: ACTION_SHOWCASE_PLUGGABILITY, getDisplayName: () => 'This is pluggable! Any plugin can inject their actions here.', - execute: async () => alert("Isn't that cool?!"), + execute: async (context: ActionExecutionContext) => + alert(`Isn't that cool?! Triggered by ${context.trigger?.id} trigger`), }); export interface PhoneContext { diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index 1b0667962a3c2f..d59309f0068385 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -97,9 +97,9 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { }); uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); setConfirmationText( - `You've successfully added a new action: ${dynamicAction.getDisplayName( - {} - )}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` + `You've successfully added a new action: ${dynamicAction.getDisplayName({ + trigger: uiActionsApi.getTrigger(HELLO_WORLD_TRIGGER_ID), + })}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` ); }} > diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts index 636ce3e623c5bf..88c1a5917e609c 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts @@ -19,6 +19,7 @@ import { createFilterAction } from './apply_filter_action'; import { expectErrorAsync } from '../../tests/helpers'; +import { defaultTrigger } from '../../../../ui_actions/public/triggers'; test('has ACTION_APPLY_FILTER type and id', () => { const action = createFilterAction(); @@ -51,6 +52,7 @@ describe('isCompatible()', () => { }), } as any, filters: [], + trigger: defaultTrigger, }); expect(result).toBe(true); }); @@ -66,6 +68,7 @@ describe('isCompatible()', () => { }), } as any, filters: [], + trigger: defaultTrigger, }); expect(result).toBe(false); }); @@ -119,6 +122,7 @@ describe('execute()', () => { await action.execute({ embeddable, filters: ['FILTER' as any], + trigger: defaultTrigger, }); expect(root.updateInput).toHaveBeenCalledTimes(1); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index cb02ffc470e95c..d8659680dceb95 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -30,6 +30,7 @@ import { PANEL_BADGE_TRIGGER, PANEL_NOTIFICATION_TRIGGER, EmbeddableContext, + contextMenuTrigger, } from '../triggers'; import { IEmbeddable, EmbeddableOutput, EmbeddableError } from '../embeddables/i_embeddable'; import { ViewMode } from '../types'; @@ -311,7 +312,11 @@ export class EmbeddablePanel extends React.Component { const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); return await buildContextMenuForActions({ - actions: sortedActions.map((action) => [action, { embeddable: this.props.embeddable }]), + actions: sortedActions.map((action) => ({ + action, + context: { embeddable: this.props.embeddable }, + trigger: contextMenuTrigger, + })), closeMenu: this.closeMyContextMenuPanel, }); }; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx index d8def3147e52c9..0361939fd07e6a 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -31,6 +31,7 @@ import { ContactCardEmbeddable } from '../../../../test_samples'; import { esFilters, Filter } from '../../../../../../../../plugins/data/public'; import { EmbeddableStart } from '../../../../../plugin'; import { embeddablePluginMock } from '../../../../../mocks'; +import { defaultTrigger } from '../../../../../../../ui_actions/public/triggers'; const { setup, doStart } = embeddablePluginMock.createInstance(); setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); @@ -85,7 +86,9 @@ test('Is not compatible when container is in view mode', async () => { () => null ); container.updateInput({ viewMode: ViewMode.VIEW }); - expect(await addPanelAction.isCompatible({ embeddable: container })).toBe(false); + expect( + await addPanelAction.isCompatible({ embeddable: container, trigger: defaultTrigger }) + ).toBe(false); }); test('Is not compatible when embeddable is not a container', async () => { @@ -94,7 +97,7 @@ test('Is not compatible when embeddable is not a container', async () => { test('Is compatible when embeddable is a parent and in edit mode', async () => { container.updateInput({ viewMode: ViewMode.EDIT }); - expect(await action.isCompatible({ embeddable: container })).toBe(true); + expect(await action.isCompatible({ embeddable: container, trigger: defaultTrigger })).toBe(true); }); test('Execute throws an error when called with an embeddable that is not a container', async () => { @@ -108,6 +111,7 @@ test('Execute throws an error when called with an embeddable that is not a conta }, {} as any ), + trigger: defaultTrigger, } as any); } await expect(check()).rejects.toThrow(Error); @@ -116,6 +120,7 @@ test('Execute does not throw an error when called with a compatible container', container.updateInput({ viewMode: ViewMode.EDIT }); await action.execute({ embeddable: container, + trigger: defaultTrigger, }); }); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index f3a483bb4bda4b..63575273bbf626 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { Action } from 'src/plugins/ui_actions/public'; +import { Action, ActionExecutionContext } from 'src/plugins/ui_actions/public'; import { NotificationsStart, OverlayStart } from 'src/core/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { ViewMode } from '../../../../types'; @@ -52,12 +52,14 @@ export class AddPanelAction implements Action { return 'plusInCircleFilled'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible(context: ActionExecutionContext) { + const { embeddable } = context; return embeddable.getIsContainer() && embeddable.getInput().viewMode === ViewMode.EDIT; } - public async execute({ embeddable }: ActionContext) { - if (!embeddable.getIsContainer() || !(await this.isCompatible({ embeddable }))) { + public async execute(context: ActionExecutionContext) { + const { embeddable } = context; + if (!embeddable.getIsContainer() || !(await this.isCompatible(context))) { throw new Error('Context is incompatible'); } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 2f086a3fb2c0cf..5d7daaa7217ed7 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { Action } from 'src/plugins/ui_actions/public'; import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; -import { EmbeddableContext } from '../../triggers'; +import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers'; export interface PanelHeaderProps { title?: string; @@ -49,11 +49,11 @@ function renderBadges(badges: Array>, embeddable: IEmb badge.execute({ embeddable })} - onClickAriaLabel={badge.getDisplayName({ embeddable })} + iconType={badge.getIconType({ embeddable, trigger: panelBadgeTrigger })} + onClick={() => badge.execute({ embeddable, trigger: panelBadgeTrigger })} + onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })} > - {badge.getDisplayName({ embeddable })} + {badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })} )); } @@ -70,14 +70,17 @@ function renderNotifications( data-test-subj={`embeddablePanelNotification-${notification.id}`} key={notification.id} style={{ marginTop: '4px', marginRight: '4px' }} - onClick={() => notification.execute(context)} + onClick={() => notification.execute({ ...context, trigger: panelNotificationTrigger })} > - {notification.getDisplayName(context)} + {notification.getDisplayName({ ...context, trigger: panelNotificationTrigger })} ); if (notification.getDisplayNameTooltip) { - const tooltip = notification.getDisplayNameTooltip(context); + const tooltip = notification.getDisplayNameTooltip({ + ...context, + trigger: panelNotificationTrigger, + }); if (tooltip) { badge = ( diff --git a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx index 0612b838a6ce70..968caf67b18262 100644 --- a/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/actions/say_hello_action.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { ActionByType, IncompatibleActionError, ActionType } from '../../ui_actions'; +import { IncompatibleActionError, ActionType, ActionDefinitionByType } from '../../ui_actions'; import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables'; // Casting to ActionType is a hack - in a real situation use @@ -42,7 +42,7 @@ export interface SayHelloActionContext { message?: string; } -export class SayHelloAction implements ActionByType { +export class SayHelloAction implements ActionDefinitionByType { public readonly type = SAY_HELLO_ACTION; public readonly id = SAY_HELLO_ACTION; diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index 9d765c99064436..f8c4a4a7e4b72f 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -31,6 +31,7 @@ import { FilterableEmbeddableInput, } from '../lib/test_samples'; import { esFilters } from '../../../data/public'; +import { applyFilterTrigger } from '../../../ui_actions/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { const { doStart, setup } = testPlugin(); @@ -85,7 +86,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a query: { match: { extension: { query: 'foo' } } }, }; - await applyFilterAction.execute({ embeddable, filters: [filter] }); + await applyFilterAction.execute({ embeddable, filters: [filter], trigger: applyFilterTrigger }); expect(root.getInput().filters.length).toBe(1); expect(node1.getInput().filters.length).toBe(1); expect(embeddable.getInput().filters.length).toBe(1); diff --git a/src/plugins/ui_actions/public/actions/action.test.ts b/src/plugins/ui_actions/public/actions/action.test.ts index f9d696d3ddb5f4..1f76223a0d7c4b 100644 --- a/src/plugins/ui_actions/public/actions/action.test.ts +++ b/src/plugins/ui_actions/public/actions/action.test.ts @@ -17,8 +17,9 @@ * under the License. */ -import { createAction } from '../../../ui_actions/public'; +import { ActionExecutionContext, createAction } from '../../../ui_actions/public'; import { ActionType } from '../types'; +import { defaultTrigger } from '../triggers'; const sayHelloAction = createAction({ // Casting to ActionType is a hack - in a real situation use @@ -29,11 +30,17 @@ const sayHelloAction = createAction({ }); test('action is not compatible based on context', async () => { - const isCompatible = await sayHelloAction.isCompatible({ amICompatible: false }); + const isCompatible = await sayHelloAction.isCompatible({ + amICompatible: false, + trigger: defaultTrigger, + } as ActionExecutionContext); expect(isCompatible).toBe(false); }); test('action is compatible based on context', async () => { - const isCompatible = await sayHelloAction.isCompatible({ amICompatible: true }); + const isCompatible = await sayHelloAction.isCompatible({ + amICompatible: true, + trigger: defaultTrigger, + } as ActionExecutionContext); expect(isCompatible).toBe(true); }); diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index bc5f36acb8f0c6..8005dadd8f5ef3 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -18,13 +18,43 @@ */ import { UiComponent } from 'src/plugins/kibana_utils/public'; -import { ActionType, ActionContextMapping } from '../types'; +import { ActionType, ActionContextMapping, BaseContext } from '../types'; import { Presentable } from '../util/presentable'; +import { Trigger } from '../triggers'; export type ActionByType = Action; +export type ActionDefinitionByType = ActionDefinition< + ActionContextMapping[T] +>; -export interface Action - extends Partial> { +/** + * During action execution we can provide additional information, + * for example, trigger, that caused the action execution + */ +export interface ActionExecutionMeta { + /** + * Trigger that executed the action + */ + trigger: Trigger; +} + +/** + * Action methods are executed with Context from trigger + {@link ActionExecutionMeta} + */ +export type ActionExecutionContext = Context & + ActionExecutionMeta; + +/** + * Simplified action context for {@link ActionDefinition} + * When defining action consumer may use either it's own Context + * or an ActionExecutionContext to get access to {@link ActionExecutionMeta} params + */ +export type ActionDefinitionContext = + | Context + | ActionExecutionContext; + +export interface Action + extends Partial>> { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. @@ -44,44 +74,51 @@ export interface Action /** * Optional EUI icon type that can be displayed along with the title. */ - getIconType(context: Context): string | undefined; + getIconType(context: ActionExecutionContext): string | undefined; /** * Returns a title to be displayed to the user. * @param context */ - getDisplayName(context: Context): string; + getDisplayName(context: ActionExecutionContext): string; /** * `UiComponent` to render when displaying this action as a context menu item. * If not provided, `getDisplayName` will be used instead. */ - MenuItem?: UiComponent<{ context: Context }>; + MenuItem?: UiComponent<{ context: ActionExecutionContext }>; /** * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. */ - isCompatible(context: Context): Promise; + isCompatible(context: ActionExecutionContext): Promise; /** * Executes the action. */ - execute(context: Context): Promise; + execute(context: ActionExecutionContext): Promise; + + /** + * This method should return a link if this item can be clicked on. The link + * is used to navigate user if user middle-clicks it or Ctrl + clicks or + * right-clicks and selects "Open in new tab". + */ + getHref?(context: ActionExecutionContext): Promise; /** * Determines if action should be executed automatically, * without first showing up in context menu. * false by default. */ - shouldAutoExecute?(context: Context): Promise; + shouldAutoExecute?(context: ActionExecutionContext): Promise; } /** * A convenience interface used to register an action. */ -export interface ActionDefinition - extends Partial> { +export interface ActionDefinition + extends Partial>> { /** * ID of the action that uniquely identifies this action in the actions registry. */ @@ -92,17 +129,30 @@ export interface ActionDefinition */ readonly type?: ActionType; + /** + * Returns a promise that resolves to true if this item is compatible given + * the context and should be displayed to user, otherwise resolves to false. + */ + isCompatible?(context: ActionDefinitionContext): Promise; + /** * Executes the action. */ - execute(context: Context): Promise; + execute(context: ActionDefinitionContext): Promise; /** * Determines if action should be executed automatically, * without first showing up in context menu. * false by default. */ - shouldAutoExecute?(context: Context): Promise; + shouldAutoExecute?(context: ActionDefinitionContext): Promise; + + /** + * This method should return a link if this item can be clicked on. The link + * is used to navigate user if user middle-clicks it or Ctrl + clicks or + * right-clicks and selects "Open in new tab". + */ + getHref?(context: ActionDefinitionContext): Promise; } export type ActionContext = A extends ActionDefinition ? Context : never; diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 7b87a5992a7f50..b44a07273f4a92 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -23,13 +23,22 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { uiToReactComponent } from '../../../kibana_react/public'; import { Action } from '../actions'; +import { Trigger } from '../triggers'; import { BaseContext } from '../types'; export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { defaultMessage: 'Options', }); -type ActionWithContext = [Action, Context]; +interface ActionWithContext { + action: Action; + context: Context; + + /** + * Trigger that caused this action + */ + trigger: Trigger; +} /** * Transforms an array of Actions to the shape EuiContextMenuPanel expects. @@ -66,15 +75,19 @@ async function buildEuiContextMenuPanelItems({ closeMenu: () => void; }) { const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length); - const promises = actions.map(async ([action, actionContext], index) => { - const isCompatible = await action.isCompatible(actionContext); + const promises = actions.map(async ({ action, context, trigger }, index) => { + const isCompatible = await action.isCompatible({ + ...context, + trigger, + }); if (!isCompatible) { return; } items[index] = await convertPanelActionToContextMenuItem({ action, - actionContext, + actionContext: context, + trigger, closeMenu, }); }); @@ -87,19 +100,30 @@ async function buildEuiContextMenuPanelItems({ async function convertPanelActionToContextMenuItem({ action, actionContext, + trigger, closeMenu, }: { action: Action; actionContext: Context; + trigger: Trigger; closeMenu: () => void; }): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { name: action.MenuItem ? React.createElement(uiToReactComponent(action.MenuItem), { - context: actionContext, + context: { + ...actionContext, + trigger, + }, }) - : action.getDisplayName(actionContext), - icon: action.getIconType(actionContext), + : action.getDisplayName({ + ...actionContext, + trigger, + }), + icon: action.getIconType({ + ...actionContext, + trigger, + }), panel: _.get(action, 'childContextMenuPanel.id'), 'data-test-subj': `embeddablePanelAction-${action.id}`, }; @@ -114,20 +138,29 @@ async function convertPanelActionToContextMenuItem({ !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys ) { event.preventDefault(); - action.execute(actionContext); + action.execute({ + ...actionContext, + trigger, + }); } else { // let browser handle navigation } } else { // not a link - action.execute(actionContext); + action.execute({ + ...actionContext, + trigger, + }); } closeMenu(); }; if (action.getHref) { - const href = await action.getHref(actionContext); + const href = await action.getHref({ + ...actionContext, + trigger, + }); if (href) { menuPanelItem.href = href; } diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index a9b413fb36542d..d76ca124ead2c6 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -45,4 +45,9 @@ export { applyFilterTrigger, } from './triggers'; export { TriggerContextMapping, TriggerId, ActionContextMapping, ActionType } from './types'; -export { ActionByType } from './actions'; +export { + ActionByType, + ActionDefinitionByType, + ActionExecutionContext, + ActionExecutionMeta, +} from './actions'; diff --git a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts index 7393989672e9de..df89c9c2f70e9d 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts @@ -46,7 +46,7 @@ export class UiActionsExecutionService { context: BaseContext; trigger: Trigger; }): Promise { - const shouldBatch = !(await action.shouldAutoExecute?.(context)) ?? false; + const shouldBatch = !(await action.shouldAutoExecute?.({ ...context, trigger })) ?? false; const task: ExecuteActionTask = { action, context, @@ -59,7 +59,7 @@ export class UiActionsExecutionService { } else { this.pendingTasks.add(task); try { - await action.execute(context); + await action.execute({ ...context, trigger }); this.pendingTasks.delete(task); } catch (e) { this.pendingTasks.delete(task); @@ -96,9 +96,12 @@ export class UiActionsExecutionService { }, 0); } - private async executeSingleTask({ context, action, defer }: ExecuteActionTask) { + private async executeSingleTask({ context, action, defer, trigger }: ExecuteActionTask) { try { - await action.execute(context); + await action.execute({ + ...context, + trigger, + }); defer.resolve(); } catch (e) { defer.reject(e); @@ -107,7 +110,11 @@ export class UiActionsExecutionService { private async executeMultipleActions(tasks: ExecuteActionTask[]) { const panel = await buildContextMenuForActions({ - actions: tasks.map(({ action, context }) => [action, context]), + actions: tasks.map(({ action, context, trigger }) => ({ + action, + context, + trigger, + })), title: tasks[0].trigger.title, // title of context menu is title of trigger which originated the chain closeMenu: () => { tasks.forEach((t) => t.defer.resolve()); diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 08efffbb6b5a8a..6028177964fb77 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -142,7 +142,7 @@ export class UiActionsService { triggerId: T, // The action can accept partial or no context, but if it needs context not provided // by this type of trigger, typescript will complain. yay! - action: Action + action: ActionDefinition | Action // TODO: remove `Action` https://github.com/elastic/kibana/issues/74501 ): void => { if (!this.actions.has(action.id)) this.registerAction(action); this.attachAction(triggerId, action.id); @@ -178,7 +178,14 @@ export class UiActionsService { context: TriggerContextMapping[T] ): Promise>> => { const actions = this.getTriggerActions!(triggerId); - const isCompatibles = await Promise.all(actions.map((action) => action.isCompatible(context))); + const isCompatibles = await Promise.all( + actions.map((action) => + action.isCompatible({ + ...context, + trigger: this.getTrigger(triggerId), + }) + ) + ); return actions.reduce( (acc: Array>, action, i) => isCompatibles[i] ? [...acc, action] : acc, diff --git a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts index 9af46f25b4fec4..81120990001e34 100644 --- a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts @@ -82,7 +82,7 @@ test('executes a single action mapped to a trigger', async () => { jest.runAllTimers(); expect(executeFn).toBeCalledTimes(1); - expect(executeFn).toBeCalledWith(context); + expect(executeFn).toBeCalledWith(expect.objectContaining(context)); }); test('throws an error if there are no compatible actions to execute', async () => { @@ -202,3 +202,25 @@ test("doesn't show a context menu for auto executable actions", async () => { expect(openContextMenu).toHaveBeenCalledTimes(0); }); }); + +test('passes trigger into execute', async () => { + const { setup, doStart } = uiActions; + const trigger = { + id: 'MY-TRIGGER' as TriggerId, + title: 'My trigger', + }; + const action = createTestAction<{ foo: string }>('test', () => true); + + setup.registerTrigger(trigger); + setup.addTriggerAction(trigger.id, action); + + const start = doStart(); + + const context = { foo: 'bar' }; + await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + jest.runAllTimers(); + expect(executeFn).toBeCalledWith({ + ...context, + trigger, + }); +}); diff --git a/src/plugins/ui_actions/public/triggers/default_trigger.ts b/src/plugins/ui_actions/public/triggers/default_trigger.ts new file mode 100644 index 00000000000000..74be0243bdac58 --- /dev/null +++ b/src/plugins/ui_actions/public/triggers/default_trigger.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Trigger } from '.'; + +export const DEFAULT_TRIGGER = ''; +export const defaultTrigger: Trigger<''> = { + id: DEFAULT_TRIGGER, + title: 'Unknown', + description: 'Unknown trigger.', +}; diff --git a/src/plugins/ui_actions/public/triggers/index.ts b/src/plugins/ui_actions/public/triggers/index.ts index a5bf9e1822941d..dbc54163c5af56 100644 --- a/src/plugins/ui_actions/public/triggers/index.ts +++ b/src/plugins/ui_actions/public/triggers/index.ts @@ -23,3 +23,4 @@ export * from './trigger_internal'; export * from './select_range_trigger'; export * from './value_click_trigger'; export * from './apply_filter_trigger'; +export * from './default_trigger'; diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 5631441cf9a1bf..dcf0bfb14d5385 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -19,7 +19,12 @@ import { ActionInternal } from './actions/action_internal'; import { TriggerInternal } from './triggers/trigger_internal'; -import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + APPLY_FILTER_TRIGGER, + DEFAULT_TRIGGER, +} from './triggers'; import type { RangeSelectContext, ValueClickContext } from '../../embeddable/public'; import type { ApplyGlobalFilterActionContext } from '../../data/public'; @@ -27,8 +32,6 @@ export type TriggerRegistry = Map>; export type ActionRegistry = Map; export type TriggerToActionsRegistry = Map; -const DEFAULT_TRIGGER = ''; - export type TriggerId = keyof TriggerContextMapping; export type BaseContext = object; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 037e017097e533..67599687dd881b 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -10,6 +10,7 @@ import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/publ import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; +import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; function isValidUrl(url: string) { try { @@ -101,7 +102,15 @@ export class DashboardToUrlDrilldown implements Drilldown return config.url; }; - public readonly execute = async (config: Config, context: ActionContext) => { + public readonly execute = async ( + config: Config, + context: ActionExecutionContext + ) => { + // Just for showcasing: + // we can get trigger a which caused this drilldown execution + // eslint-disable-next-line no-console + console.log(context.trigger?.id); + const url = await this.getHref(config, context); if (config.openInNewTab) { diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index d0800c7b24fef4..30025dce18c0bf 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -8,7 +8,10 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; -import { Action, IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; +import { + UiActionsActionDefinition as ActionDefinition, + IncompatibleActionError, +} from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { checkLicense } from '../lib/license_check'; @@ -30,7 +33,7 @@ interface ActionContext { embeddable: ISearchEmbeddable; } -export class GetCsvReportPanelAction implements Action { +export class GetCsvReportPanelAction implements ActionDefinition { private isDownloading: boolean; public readonly type = ''; public readonly id = CSV_REPORTING_ACTION; diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 5d9804d2a5c33c..259fe5c774c4b9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { ActionByType, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { + ActionDefinitionByType, + IncompatibleActionError, +} from '../../../../src/plugins/ui_actions/public'; import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { OpenModal, CommonlyUsedRange } from './types'; @@ -38,7 +41,7 @@ export interface TimeRangeActionContext { embeddable: Embeddable; } -export class CustomTimeRangeAction implements ActionByType { +export class CustomTimeRangeAction implements ActionDefinitionByType { public readonly type = CUSTOM_TIME_RANGE; private openModal: OpenModal; private dateFormat?: string; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index a41ae851e185b0..756bdf9e672aac 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -6,6 +6,7 @@ import { ActionFactoryDefinition } from '../dynamic_actions'; import { LicenseType } from '../../../licensing/public'; +import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; /** * This is a convenience interface to register a drilldown. Drilldown has @@ -93,10 +94,16 @@ export interface DrilldownDefinition< * @param context Object that represents context in which the underlying * `UIAction` of this drilldown is being executed in. */ - execute(config: Config, context: ExecutionContext): void; + execute( + config: Config, + context: ExecutionContext | ActionExecutionContext + ): void; /** * A link where drilldown should navigate on middle click or Ctrl + click. */ - getHref?(config: Config, context: ExecutionContext): Promise; + getHref?( + config: Config, + context: ExecutionContext | ActionExecutionContext + ): Promise; } From 67e28ac8b45df85c18fe71902833a0c5bd36fe2d Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Fri, 14 Aug 2020 08:34:26 -0400 Subject: [PATCH 17/46] [EventLog] Populate alert instances view with event log data (#68437) resolves https://github.com/elastic/kibana/issues/57446 Adds a new API (AlertClient and HTTP endpoint) `getAlertStatus()` which returns alert data calculated from the event log. --- x-pack/plugins/alerts/README.md | 18 + x-pack/plugins/alerts/common/alert_status.ts | 31 ++ x-pack/plugins/alerts/common/index.ts | 1 + .../alerts/server/alerts_client.mock.ts | 1 + .../alerts/server/alerts_client.test.ts | 246 +++++++++- x-pack/plugins/alerts/server/alerts_client.ts | 80 ++- .../server/alerts_client_factory.test.ts | 4 + .../alerts/server/alerts_client_factory.ts | 9 +- .../authorization/alerts_authorization.ts | 1 + .../lib/alert_status_from_event_log.test.ts | 464 ++++++++++++++++++ .../server/lib/alert_status_from_event_log.ts | 123 +++++ .../server/lib/iso_or_relative_date.test.ts | 28 ++ .../alerts/server/lib/iso_or_relative_date.ts | 27 + x-pack/plugins/alerts/server/plugin.ts | 9 +- .../server/routes/get_alert_status.test.ts | 105 ++++ .../alerts/server/routes/get_alert_status.ts | 52 ++ x-pack/plugins/alerts/server/routes/index.ts | 1 + .../server/task_runner/task_runner.test.ts | 67 ++- .../alerts/server/task_runner/task_runner.ts | 30 +- .../server/event_log_start_service.test.ts | 8 + x-pack/plugins/event_log/server/index.ts | 3 + x-pack/plugins/event_log/server/mocks.ts | 2 + x-pack/plugins/event_log/server/types.ts | 1 + .../alerting.test.ts | 4 + .../feature_privilege_builder/alerting.ts | 2 +- .../public/application/lib/alert_api.ts | 12 +- .../components/alert_instances.test.tsx | 149 +++--- .../components/alert_instances.tsx | 49 +- .../components/alert_instances_route.test.tsx | 75 +-- .../components/alert_instances_route.tsx | 30 +- .../with_bulk_alert_api_operations.tsx | 11 +- .../triggers_actions_ui/public/types.ts | 12 +- .../plugins/alerts/server/alert_types.ts | 22 +- .../common/lib/get_event_log.ts | 2 +- .../tests/alerting/get_alert_status.ts | 202 ++++++++ .../tests/alerting/index.ts | 1 + .../spaces_only/tests/alerting/event_log.ts | 35 +- .../tests/alerting/get_alert_status.ts | 261 ++++++++++ .../spaces_only/tests/alerting/index.ts | 1 + .../apps/triggers_actions_ui/details.ts | 20 +- .../services/alerting/alerts.ts | 19 +- 41 files changed, 2012 insertions(+), 206 deletions(-) create mode 100644 x-pack/plugins/alerts/common/alert_status.ts create mode 100644 x-pack/plugins/alerts/server/lib/alert_status_from_event_log.test.ts create mode 100644 x-pack/plugins/alerts/server/lib/alert_status_from_event_log.ts create mode 100644 x-pack/plugins/alerts/server/lib/iso_or_relative_date.test.ts create mode 100644 x-pack/plugins/alerts/server/lib/iso_or_relative_date.ts create mode 100644 x-pack/plugins/alerts/server/routes/get_alert_status.test.ts create mode 100644 x-pack/plugins/alerts/server/routes/get_alert_status.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_status.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_status.ts diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 10568abbe3c72c..aab05cb0a7cfd4 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -26,6 +26,7 @@ Table of Contents - [`GET /api/alerts/_find`: Find alerts](#get-apialertfind-find-alerts) - [`GET /api/alerts/alert/{id}`: Get alert](#get-apialertid-get-alert) - [`GET /api/alerts/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) + - [`GET /api/alerts/alert/{id}/status`: Get alert status](#get-apialertidstate-get-alert-status) - [`GET /api/alerts/list_alert_types`: List alert types](#get-apialerttypes-list-alert-types) - [`PUT /api/alerts/alert/{id}`: Update alert](#put-apialertid-update-alert) - [`POST /api/alerts/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) @@ -504,6 +505,23 @@ Params: |---|---|---| |id|The id of the alert whose state you're trying to get.|string| +### `GET /api/alerts/alert/{id}/status`: Get alert status + +Similar to the `GET state` call, but collects additional information from +the event log. + +Params: + +|Property|Description|Type| +|---|---|---| +|id|The id of the alert whose status you're trying to get.|string| + +Query: + +|Property|Description|Type| +|---|---|---| +|dateStart|The date to start looking for alert events in the event log. Either an ISO date string, or a duration string indicating time since now.|string| + ### `GET /api/alerts/list_alert_types`: List alert types No parameters. diff --git a/x-pack/plugins/alerts/common/alert_status.ts b/x-pack/plugins/alerts/common/alert_status.ts new file mode 100644 index 00000000000000..517db6d6cb243c --- /dev/null +++ b/x-pack/plugins/alerts/common/alert_status.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +type AlertStatusValues = 'OK' | 'Active' | 'Error'; +type AlertInstanceStatusValues = 'OK' | 'Active'; + +export interface AlertStatus { + id: string; + name: string; + tags: string[]; + alertTypeId: string; + consumer: string; + muteAll: boolean; + throttle: string | null; + enabled: boolean; + statusStartDate: string; + statusEndDate: string; + status: AlertStatusValues; + lastRun?: string; + errorMessages: Array<{ date: string; message: string }>; + instances: Record; +} + +export interface AlertInstanceStatus { + status: AlertInstanceStatusValues; + muted: boolean; + activeStartDate?: string; +} diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index b839c07a9db89a..0922e164a3aa3f 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -9,6 +9,7 @@ export * from './alert_type'; export * from './alert_instance'; export * from './alert_task_instance'; export * from './alert_navigation'; +export * from './alert_status'; export interface ActionGroup { id: string; diff --git a/x-pack/plugins/alerts/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts index be70e441b6fc5c..b61139ae72c995 100644 --- a/x-pack/plugins/alerts/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerts/server/alerts_client.mock.ts @@ -25,6 +25,7 @@ const createAlertsClientMock = () => { muteInstance: jest.fn(), unmuteInstance: jest.fn(), listAlertTypes: jest.fn(), + getAlertStatus: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index c25e040ad09ce8..d994269366ae6c 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -11,16 +11,22 @@ import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; import { TaskStatus } from '../../task_manager/server'; -import { IntervalSchedule } from './types'; +import { IntervalSchedule, RawAlert } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks'; import { AlertsAuthorization } from './authorization/alerts_authorization'; import { ActionsAuthorization } from '../../actions/server'; +import { eventLogClientMock } from '../../event_log/server/mocks'; +import { QueryEventsBySavedObjectResult } from '../../event_log/server'; +import { SavedObject } from 'kibana/server'; +import { EventsFactory } from './lib/alert_status_from_event_log.test'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const eventLogClient = eventLogClientMock.create(); + const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertsAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); @@ -39,6 +45,7 @@ const alertsClientParams: jest.Mocked = { logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), }; beforeEach(() => { @@ -91,17 +98,33 @@ beforeEach(() => { async executor() {}, producer: 'alerts', })); + alertsClientParams.getEventLogClient.mockResolvedValue(eventLogClient); }); -const mockedDate = new Date('2019-02-12T21:01:22.479Z'); -// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockedDateString = '2019-02-12T21:01:22.479Z'; +const mockedDate = new Date(mockedDateString); +const DateOriginal = Date; + +// A version of date that responds to `new Date(null|undefined)` and `Date.now()` +// by returning a fixed date, otherwise should be same as Date. +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ (global as any).Date = class Date { - constructor() { - return mockedDate; + constructor(...args: unknown[]) { + // sometimes the ctor has no args, sometimes has a single `null` arg + if (args[0] == null) { + // @ts-ignore + return mockedDate; + } else { + // @ts-ignore + return new DateOriginal(...args); + } } static now() { return mockedDate.getTime(); } + static parse(string: string) { + return DateOriginal.parse(string); + } }; function getMockData(overwrites: Record = {}): CreateOptions['data'] { @@ -2295,6 +2318,219 @@ describe('getAlertState()', () => { }); }); +const AlertStatusFindEventsResult: QueryEventsBySavedObjectResult = { + page: 1, + per_page: 10000, + total: 0, + data: [], +}; + +const AlertStatusIntervalSeconds = 1; + +const BaseAlertStatusSavedObject: SavedObject = { + id: '1', + type: 'alert', + attributes: { + enabled: true, + name: 'alert-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: `${AlertStatusIntervalSeconds}s` }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: mockedDateString, + apiKey: null, + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + }, + references: [], +}; + +function getAlertStatusSavedObject(attributes: Partial = {}): SavedObject { + return { + ...BaseAlertStatusSavedObject, + attributes: { ...BaseAlertStatusSavedObject.attributes, ...attributes }, + }; +} + +describe('getAlertStatus()', () => { + let alertsClient: AlertsClient; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + test('runs as expected with some event log data', async () => { + const alertSO = getAlertStatusSavedObject({ mutedInstanceIds: ['instance-muted-no-activity'] }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO); + + const eventsFactory = new EventsFactory(mockedDateString); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-currently-active') + .addNewInstance('instance-previously-active') + .addActiveInstance('instance-currently-active') + .addActiveInstance('instance-previously-active') + .advanceTime(10000) + .addExecute() + .addResolvedInstance('instance-previously-active') + .addActiveInstance('instance-currently-active') + .getEvents(); + const eventsResult = { + ...AlertStatusFindEventsResult, + total: events.length, + data: events, + }; + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult); + + const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); + + const result = await alertsClient.getAlertStatus({ id: '1', dateStart }); + expect(result).toMatchInlineSnapshot(` + Object { + "alertTypeId": "123", + "consumer": "alert-consumer", + "enabled": true, + "errorMessages": Array [], + "id": "1", + "instances": Object { + "instance-currently-active": Object { + "activeStartDate": "2019-02-12T21:01:22.479Z", + "muted": false, + "status": "Active", + }, + "instance-muted-no-activity": Object { + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + "instance-previously-active": Object { + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2019-02-12T21:01:32.479Z", + "muteAll": false, + "name": "alert-name", + "status": "Active", + "statusEndDate": "2019-02-12T21:01:22.479Z", + "statusStartDate": "2019-02-12T21:00:22.479Z", + "tags": Array [ + "tag-1", + "tag-2", + ], + "throttle": null, + } + `); + }); + + // Further tests don't check the result of `getAlertStatus()`, as the result + // is just the result from the `alertStatusFromEventLog()`, which itself + // has a complete set of tests. These tests just make sure the data gets + // sent into `getAlertStatus()` as appropriate. + + test('calls saved objects and event log client with default params', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult); + + await alertsClient.getAlertStatus({ id: '1' }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + Object { + "end": "2019-02-12T21:01:22.479Z", + "page": 1, + "per_page": 10000, + "sort_order": "desc", + "start": "2019-02-12T21:00:22.479Z", + }, + ] + `); + // calculate the expected start/end date for one test + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + expect(end).toBe(mockedDateString); + + const startMillis = Date.parse(start!); + const endMillis = Date.parse(end!); + const expectedDuration = 60 * AlertStatusIntervalSeconds * 1000; + expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); + expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); + }); + + test('calls event log client with start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult); + + const dateStart = new Date(Date.now() - 60 * AlertStatusIntervalSeconds * 1000).toISOString(); + await alertsClient.getAlertStatus({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T21:00:22.479Z", + } + `); + }); + + test('calls event log client with relative start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult); + + const dateStart = '2m'; + await alertsClient.getAlertStatus({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T20:59:22.479Z", + } + `); + }); + + test('invalid start date throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult); + + const dateStart = 'ain"t no way this will get parsed as a date'; + expect(alertsClient.getAlertStatus({ id: '1', dateStart })).rejects.toMatchInlineSnapshot( + `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` + ); + }); + + test('saved object get throws an error', async () => { + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult); + + expect(alertsClient.getAlertStatus({ id: '1' })).rejects.toMatchInlineSnapshot(`[Error: OMG!]`); + }); + + test('findEvents throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject()); + eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!')); + + // error eaten but logged + await alertsClient.getAlertStatus({ id: '1' }); + }); +}); + describe('find()', () => { const listedTypes = new Set([ { diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index dd66ccc7a0256d..80e021fc5cb6e1 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -24,6 +24,7 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, + AlertStatus, } from './types'; import { validateAlertTypeParams } from './lib'; import { @@ -41,6 +42,11 @@ import { WriteOperations, ReadOperations, } from './authorization/alerts_authorization'; +import { IEventLogClient } from '../../../plugins/event_log/server'; +import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date'; +import { alertStatusFromEventLog } from './lib/alert_status_from_event_log'; +import { IEvent } from '../../event_log/server'; +import { parseDuration } from '../common/parse_duration'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -67,6 +73,7 @@ export interface ConstructorOptions { createAPIKey: (name: string) => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; getActionsClient: () => Promise; + getEventLogClient: () => Promise; } export interface MuteOptions extends IndexType { @@ -132,6 +139,11 @@ interface UpdateOptions { }; } +interface GetAlertStatusParams { + id: string; + dateStart?: string; +} + export class AlertsClient { private readonly logger: Logger; private readonly getUserName: () => Promise; @@ -147,6 +159,7 @@ export class AlertsClient { ) => Promise; private readonly getActionsClient: () => Promise; private readonly actionsAuthorization: ActionsAuthorization; + private readonly getEventLogClient: () => Promise; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; constructor({ @@ -163,6 +176,7 @@ export class AlertsClient { encryptedSavedObjectsClient, getActionsClient, actionsAuthorization, + getEventLogClient, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -177,6 +191,7 @@ export class AlertsClient { this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; this.getActionsClient = getActionsClient; this.actionsAuthorization = actionsAuthorization; + this.getEventLogClient = getEventLogClient; } public async create({ data, options }: CreateOptions): Promise { @@ -269,6 +284,49 @@ export class AlertsClient { } } + public async getAlertStatus({ id, dateStart }: GetAlertStatusParams): Promise { + this.logger.debug(`getAlertStatus(): getting alert ${id}`); + const alert = await this.get({ id }); + await this.authorization.ensureAuthorized( + alert.alertTypeId, + alert.consumer, + ReadOperations.GetAlertStatus + ); + + // default duration of status is 60 * alert interval + const dateNow = new Date(); + const durationMillis = parseDuration(alert.schedule.interval) * 60; + const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); + const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); + + const eventLogClient = await this.getEventLogClient(); + + this.logger.debug(`getAlertStatus(): search the event log for alert ${id}`); + let events: IEvent[]; + try { + const queryResults = await eventLogClient.findEventsBySavedObject('alert', id, { + page: 1, + per_page: 10000, + start: parsedDateStart.toISOString(), + end: dateNow.toISOString(), + sort_order: 'desc', + }); + events = queryResults.data; + } catch (err) { + this.logger.debug( + `alertsClient.getAlertStatus(): error searching event log for alert ${id}: ${err.message}` + ); + events = []; + } + + return alertStatusFromEventLog({ + alert, + events, + dateStart: parsedDateStart.toISOString(), + dateEnd: dateNow.toISOString(), + }); + } + public async find({ options: { fields, ...options } = {}, }: { options?: FindOptions } = {}): Promise { @@ -283,7 +341,6 @@ export class AlertsClient { ? `${options.filter} and ${authorizationFilter}` : authorizationFilter; } - const { page, per_page: perPage, @@ -886,3 +943,24 @@ export class AlertsClient { return truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 }); } } + +function parseDate(dateString: string | undefined, propertyName: string, defaultValue: Date): Date { + if (dateString === undefined) { + return defaultValue; + } + + const parsedDate = parseIsoOrRelativeDate(dateString); + if (parsedDate === undefined) { + throw Boom.badRequest( + i18n.translate('xpack.alerts.alertsClient.getAlertStatus.invalidDate', { + defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"', + values: { + field: propertyName, + dateValue: dateString, + }, + }) + ); + } + + return parsedDate; +} diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 16b5af499bb90f..a5eb371633f1ed 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -22,6 +22,7 @@ import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mock import { featuresPluginMock } from '../../features/server/mocks'; import { AuditLogger } from '../../security/server'; import { ALERTS_FEATURE_ID } from '../common'; +import { eventLogMock } from '../../event_log/server/mocks'; jest.mock('./alerts_client'); jest.mock('./authorization/alerts_authorization'); @@ -42,6 +43,7 @@ const alertsClientFactoryParams: jest.Mocked = { encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), actions: actionsMock.createStart(), features, + eventLog: eventLogMock.createStart(), }; const fakeRequest = ({ headers: {}, @@ -119,6 +121,7 @@ test('creates an alerts client with proper constructor arguments when security i namespace: 'default', getUserName: expect.any(Function), getActionsClient: expect.any(Function), + getEventLogClient: expect.any(Function), createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, @@ -164,6 +167,7 @@ test('creates an alerts client with proper constructor arguments', async () => { invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, getActionsClient: expect.any(Function), + getEventLogClient: expect.any(Function), }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 79b0ccaf1f0bc0..83202424c97733 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -16,6 +16,7 @@ import { PluginStartContract as FeaturesPluginStart } from '../../features/serve import { AlertsAuthorization } from './authorization/alerts_authorization'; import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; import { Space } from '../../spaces/server'; +import { IEventLogClientService } from '../../../plugins/event_log/server'; export interface AlertsClientFactoryOpts { logger: Logger; @@ -28,6 +29,7 @@ export interface AlertsClientFactoryOpts { encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actions: ActionsPluginStartContract; features: FeaturesPluginStart; + eventLog: IEventLogClientService; } export class AlertsClientFactory { @@ -42,6 +44,7 @@ export class AlertsClientFactory { private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; private actions!: ActionsPluginStartContract; private features!: FeaturesPluginStart; + private eventLog!: IEventLogClientService; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -58,10 +61,11 @@ export class AlertsClientFactory { this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; this.actions = options.actions; this.features = options.features; + this.eventLog = options.eventLog; } public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { - const { securityPluginSetup, actions, features } = this; + const { securityPluginSetup, actions, eventLog, features } = this; const spaceId = this.getSpaceId(request); const authorization = new AlertsAuthorization({ authorization: securityPluginSetup?.authz, @@ -135,6 +139,9 @@ export class AlertsClientFactory { async getActionsClient() { return actions.getActionsClientWithRequest(request); }, + async getEventLogClient() { + return eventLog.getClient(request); + }, }); } } diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 33a9a0bf0396ea..b2a214eae93166 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -18,6 +18,7 @@ import { Space } from '../../../spaces/server'; export enum ReadOperations { Get = 'get', GetAlertState = 'getAlertState', + GetAlertStatus = 'getAlertStatus', Find = 'find', } diff --git a/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.test.ts new file mode 100644 index 00000000000000..15570d3032f240 --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.test.ts @@ -0,0 +1,464 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SanitizedAlert, AlertStatus } from '../types'; +import { IValidatedEvent } from '../../../event_log/server'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; +import { alertStatusFromEventLog } from './alert_status_from_event_log'; + +const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; +const dateStart = '2020-06-18T00:00:00.000Z'; +const dateEnd = dateString(dateStart, ONE_HOUR_IN_MILLIS); + +describe('alertStatusFromEventLog', () => { + test('no events and muted ids', async () => { + const alert = createAlert({}); + const events: IValidatedEvent[] = []; + const status: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + expect(status).toMatchInlineSnapshot(` + Object { + "alertTypeId": "123", + "consumer": "alert-consumer", + "enabled": false, + "errorMessages": Array [], + "id": "alert-123", + "instances": Object {}, + "lastRun": undefined, + "muteAll": false, + "name": "alert-name", + "status": "OK", + "statusEndDate": "2020-06-18T01:00:00.000Z", + "statusStartDate": "2020-06-18T00:00:00.000Z", + "tags": Array [], + "throttle": null, + } + `); + }); + + test('different alert properties', async () => { + const alert = createAlert({ + id: 'alert-456', + alertTypeId: '456', + schedule: { interval: '100s' }, + enabled: true, + name: 'alert-name-2', + tags: ['tag-1', 'tag-2'], + consumer: 'alert-consumer-2', + throttle: '1h', + muteAll: true, + }); + const events: IValidatedEvent[] = []; + const status: AlertStatus = alertStatusFromEventLog({ + alert, + events, + dateStart: dateString(dateEnd, ONE_HOUR_IN_MILLIS), + dateEnd: dateString(dateEnd, ONE_HOUR_IN_MILLIS * 2), + }); + + expect(status).toMatchInlineSnapshot(` + Object { + "alertTypeId": "456", + "consumer": "alert-consumer-2", + "enabled": true, + "errorMessages": Array [], + "id": "alert-456", + "instances": Object {}, + "lastRun": undefined, + "muteAll": true, + "name": "alert-name-2", + "status": "OK", + "statusEndDate": "2020-06-18T03:00:00.000Z", + "statusStartDate": "2020-06-18T02:00:00.000Z", + "tags": Array [ + "tag-1", + "tag-2", + ], + "throttle": "1h", + } + `); + }); + + test('two muted instances', async () => { + const alert = createAlert({ + mutedInstanceIds: ['instance-1', 'instance-2'], + }); + const events: IValidatedEvent[] = []; + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + "instance-2": Object { + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + }, + "lastRun": undefined, + "status": "OK", + } + `); + }); + + test('active alert but no instances', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory.addExecute().advanceTime(10000).addExecute().getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object {}, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + }); + + test('active alert with no instances but has errors', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute('oof!') + .advanceTime(10000) + .addExecute('rut roh!') + .getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, errorMessages, instances } = alertStatus; + expect({ lastRun, status, errorMessages, instances }).toMatchInlineSnapshot(` + Object { + "errorMessages": Array [ + Object { + "date": "2020-06-18T00:00:00.000Z", + "message": "oof!", + }, + Object { + "date": "2020-06-18T00:00:10.000Z", + "message": "rut roh!", + }, + ], + "instances": Object {}, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Error", + } + `); + }); + + test('alert with currently inactive instance', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1') + .advanceTime(10000) + .addExecute() + .addResolvedInstance('instance-1') + .getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + }); + + test('alert with currently inactive instance, no new-instance', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addActiveInstance('instance-1') + .advanceTime(10000) + .addExecute() + .addResolvedInstance('instance-1') + .getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + }); + + test('alert with currently active instance', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1') + .getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + }); + + test('alert with currently active instance, no new-instance', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addActiveInstance('instance-1') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1') + .getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "activeStartDate": undefined, + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + }); + + test('alert with active and inactive muted alerts', async () => { + const alert = createAlert({ mutedInstanceIds: ['instance-1', 'instance-2'] }); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1') + .addNewInstance('instance-2') + .addActiveInstance('instance-2') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1') + .addResolvedInstance('instance-2') + .getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": true, + "status": "Active", + }, + "instance-2": Object { + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + }); + + test('alert with active and inactive alerts over many executes', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1') + .addNewInstance('instance-2') + .addActiveInstance('instance-2') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1') + .addResolvedInstance('instance-2') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1') + .getEvents(); + + const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd }); + + const { lastRun, status, instances } = alertStatus; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + "instance-2": Object { + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:30.000Z", + "status": "Active", + } + `); + }); +}); + +function dateString(isoBaseDate: string, offsetMillis = 0): string { + return new Date(Date.parse(isoBaseDate) + offsetMillis).toISOString(); +} + +export class EventsFactory { + private events: IValidatedEvent[] = []; + + constructor(private date: string = dateStart) {} + + getEvents(): IValidatedEvent[] { + // ES normally returns events sorted newest to oldest, so we need to sort + // that way also + const events = this.events.slice(); + events.sort((a, b) => -a!['@timestamp']!.localeCompare(b!['@timestamp']!)); + return events; + } + + getTime(): string { + return this.date; + } + + advanceTime(millis: number): EventsFactory { + this.date = dateString(this.date, millis); + return this; + } + + addExecute(errorMessage?: string): EventsFactory { + let event: IValidatedEvent = { + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.execute, + }, + }; + + if (errorMessage) { + event = { ...event, error: { message: errorMessage } }; + } + + this.events.push(event); + return this; + } + + addActiveInstance(instanceId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.activeInstance, + }, + kibana: { alerting: { instance_id: instanceId } }, + }); + return this; + } + + addNewInstance(instanceId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.newInstance, + }, + kibana: { alerting: { instance_id: instanceId } }, + }); + return this; + } + + addResolvedInstance(instanceId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.resolvedInstance, + }, + kibana: { alerting: { instance_id: instanceId } }, + }); + return this; + } +} + +function createAlert(overrides: Partial): SanitizedAlert { + return { ...BaseAlert, ...overrides }; +} + +const BaseAlert: SanitizedAlert = { + id: 'alert-123', + alertTypeId: '123', + schedule: { interval: '10s' }, + enabled: false, + name: 'alert-name', + tags: [], + consumer: 'alert-consumer', + throttle: null, + muteAll: false, + mutedInstanceIds: [], + params: { bar: true }, + actions: [], + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, +}; diff --git a/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.ts new file mode 100644 index 00000000000000..606bd44c6990ca --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SanitizedAlert, AlertStatus, AlertInstanceStatus } from '../types'; +import { IEvent } from '../../../event_log/server'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; + +export interface AlertStatusFromEventLogParams { + alert: SanitizedAlert; + events: IEvent[]; + dateStart: string; + dateEnd: string; +} + +export function alertStatusFromEventLog(params: AlertStatusFromEventLogParams): AlertStatus { + // initialize the result + const { alert, events, dateStart, dateEnd } = params; + const alertStatus: AlertStatus = { + id: alert.id, + name: alert.name, + tags: alert.tags, + alertTypeId: alert.alertTypeId, + consumer: alert.consumer, + statusStartDate: dateStart, + statusEndDate: dateEnd, + status: 'OK', + muteAll: alert.muteAll, + throttle: alert.throttle, + enabled: alert.enabled, + lastRun: undefined, + errorMessages: [], + instances: {}, + }; + + const instances = new Map(); + + // loop through the events + // should be sorted newest to oldest, we want oldest to newest, so reverse + for (const event of events.reverse()) { + const timeStamp = event?.['@timestamp']; + if (timeStamp === undefined) continue; + + const provider = event?.event?.provider; + if (provider !== EVENT_LOG_PROVIDER) continue; + + const action = event?.event?.action; + if (action === undefined) continue; + + if (action === EVENT_LOG_ACTIONS.execute) { + alertStatus.lastRun = timeStamp; + + const errorMessage = event?.error?.message; + if (errorMessage !== undefined) { + alertStatus.status = 'Error'; + alertStatus.errorMessages.push({ + date: timeStamp, + message: errorMessage, + }); + } else { + alertStatus.status = 'OK'; + } + + continue; + } + + const instanceId = event?.kibana?.alerting?.instance_id; + if (instanceId === undefined) continue; + + const status = getAlertInstanceStatus(instances, instanceId); + switch (action) { + case EVENT_LOG_ACTIONS.newInstance: + status.activeStartDate = timeStamp; + // intentionally no break here + case EVENT_LOG_ACTIONS.activeInstance: + status.status = 'Active'; + break; + case EVENT_LOG_ACTIONS.resolvedInstance: + status.status = 'OK'; + status.activeStartDate = undefined; + } + } + + // set the muted status of instances + for (const instanceId of alert.mutedInstanceIds) { + getAlertInstanceStatus(instances, instanceId).muted = true; + } + + // convert the instances map to object form + const instanceIds = Array.from(instances.keys()).sort(); + for (const instanceId of instanceIds) { + alertStatus.instances[instanceId] = instances.get(instanceId)!; + } + + // set the overall alert status to Active if appropriate + if (alertStatus.status !== 'Error') { + if (Array.from(instances.values()).some((instance) => instance.status === 'Active')) { + alertStatus.status = 'Active'; + } + } + + alertStatus.errorMessages.sort((a, b) => a.date.localeCompare(b.date)); + + return alertStatus; +} + +// return an instance status object, creating and adding to the map if needed +function getAlertInstanceStatus( + instances: Map, + instanceId: string +): AlertInstanceStatus { + if (instances.has(instanceId)) return instances.get(instanceId)!; + + const status: AlertInstanceStatus = { + status: 'OK', + muted: false, + activeStartDate: undefined, + }; + instances.set(instanceId, status); + return status; +} diff --git a/x-pack/plugins/alerts/server/lib/iso_or_relative_date.test.ts b/x-pack/plugins/alerts/server/lib/iso_or_relative_date.test.ts new file mode 100644 index 00000000000000..91272c1cca3b5a --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/iso_or_relative_date.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseIsoOrRelativeDate } from './iso_or_relative_date'; + +describe('parseIsoOrRelativeDate', () => { + test('handles ISO dates', () => { + const date = new Date(); + const parsedDate = parseIsoOrRelativeDate(date.toISOString()); + expect(parsedDate?.valueOf()).toBe(date.valueOf()); + }); + + test('handles relative dates', () => { + const hoursDiff = 1; + const date = new Date(Date.now() - hoursDiff * 60 * 60 * 1000); + const parsedDate = parseIsoOrRelativeDate(`${hoursDiff}h`); + const diff = Math.abs(parsedDate!.valueOf() - date.valueOf()); + expect(diff).toBeLessThan(1000); + }); + + test('returns undefined for invalid date strings', () => { + const parsedDate = parseIsoOrRelativeDate('this shall not pass'); + expect(parsedDate).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/alerts/server/lib/iso_or_relative_date.ts b/x-pack/plugins/alerts/server/lib/iso_or_relative_date.ts new file mode 100644 index 00000000000000..77c4eefa044394 --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/iso_or_relative_date.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseDuration } from '../../common/parse_duration'; + +/** + * Parse an ISO date or NNx duration string as a Date + * + * @param dateString an ISO date or NNx "duration" string representing now-duration + * @returns a Date or undefined if the dateString was not valid + */ +export function parseIsoOrRelativeDate(dateString: string): Date | undefined { + const epochMillis = Date.parse(dateString); + if (!isNaN(epochMillis)) return new Date(epochMillis); + + let millis: number; + try { + millis = parseDuration(dateString); + } catch (err) { + return; + } + + return new Date(Date.now() - millis); +} diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 5d69887bd5bf0a..d5843bd531d848 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -38,6 +38,7 @@ import { findAlertRoute, getAlertRoute, getAlertStateRoute, + getAlertStatusRoute, listAlertTypesRoute, updateAlertRoute, enableAlertRoute, @@ -57,16 +58,17 @@ import { import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; -import { IEventLogger, IEventLogService } from '../../event_log/server'; +import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; import { setupSavedObjects } from './saved_objects'; -const EVENT_LOG_PROVIDER = 'alerting'; +export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { execute: 'execute', executeAction: 'execute-action', newInstance: 'new-instance', resolvedInstance: 'resolved-instance', + activeInstance: 'active-instance', }; export interface PluginSetupContract { @@ -92,6 +94,7 @@ export interface AlertingPluginsStart { taskManager: TaskManagerStartContract; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; features: FeaturesPluginStart; + eventLog: IEventLogClientService; } export class AlertingPlugin { @@ -189,6 +192,7 @@ export class AlertingPlugin { findAlertRoute(router, this.licenseState); getAlertRoute(router, this.licenseState); getAlertStateRoute(router, this.licenseState); + getAlertStatusRoute(router, this.licenseState); listAlertTypesRoute(router, this.licenseState); updateAlertRoute(router, this.licenseState); enableAlertRoute(router, this.licenseState); @@ -235,6 +239,7 @@ export class AlertingPlugin { }, actions: plugins.actions, features: plugins.features, + eventLog: plugins.eventLog, }); const getAlertsClientWithRequest = (request: KibanaRequest) => { diff --git a/x-pack/plugins/alerts/server/routes/get_alert_status.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_status.test.ts new file mode 100644 index 00000000000000..1b4cb1941018ba --- /dev/null +++ b/x-pack/plugins/alerts/server/routes/get_alert_status.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getAlertStatusRoute } from './get_alert_status'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { mockLicenseState } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; +import { alertsClientMock } from '../alerts_client.mock'; +import { AlertStatus } from '../types'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getAlertStatusRoute', () => { + const dateString = new Date().toISOString(); + const mockedAlertStatus: AlertStatus = { + id: '', + name: '', + tags: [], + alertTypeId: '', + consumer: '', + muteAll: false, + throttle: null, + enabled: false, + statusStartDate: dateString, + statusEndDate: dateString, + status: 'OK', + errorMessages: [], + instances: {}, + }; + + it('gets alert status', async () => { + const licenseState = mockLicenseState(); + const router = httpServiceMock.createRouter(); + + getAlertStatusRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/status"`); + + alertsClient.getAlertStatus.mockResolvedValueOnce(mockedAlertStatus); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + query: {}, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(alertsClient.getAlertStatus).toHaveBeenCalledTimes(1); + expect(alertsClient.getAlertStatus.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "dateStart": undefined, + "id": "1", + }, + ] + `); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('returns NOT-FOUND when alert is not found', async () => { + const licenseState = mockLicenseState(); + const router = httpServiceMock.createRouter(); + + getAlertStatusRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + alertsClient.getAlertStatus = jest + .fn() + .mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { + id: '1', + }, + query: {}, + }, + ['notFound'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/alerts/server/routes/get_alert_status.ts b/x-pack/plugins/alerts/server/routes/get_alert_status.ts new file mode 100644 index 00000000000000..eab18c50189f45 --- /dev/null +++ b/x-pack/plugins/alerts/server/routes/get_alert_status.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { LicenseState } from '../lib/license_state'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { BASE_ALERT_API_PATH } from '../../common'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const querySchema = schema.object({ + dateStart: schema.maybe(schema.string()), +}); + +export const getAlertStatusRoute = (router: IRouter, licenseState: LicenseState) => { + router.get( + { + path: `${BASE_ALERT_API_PATH}/alert/{id}/status`, + validate: { + params: paramSchema, + query: querySchema, + }, + }, + router.handleLegacyErrors(async function ( + context: RequestHandlerContext, + req: KibanaRequest, TypeOf, unknown>, + res: KibanaResponseFactory + ): Promise { + verifyApiAccess(licenseState); + if (!context.alerting) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); + } + const alertsClient = context.alerting.getAlertsClient(); + const { id } = req.params; + const { dateStart } = req.query; + const status = await alertsClient.getAlertStatus({ id, dateStart }); + return res.ok({ body: status }); + }) + ); +}; diff --git a/x-pack/plugins/alerts/server/routes/index.ts b/x-pack/plugins/alerts/server/routes/index.ts index f833a29c67bb90..4c6b1eb8e9b587 100644 --- a/x-pack/plugins/alerts/server/routes/index.ts +++ b/x-pack/plugins/alerts/server/routes/index.ts @@ -9,6 +9,7 @@ export { deleteAlertRoute } from './delete'; export { findAlertRoute } from './find'; export { getAlertRoute } from './get'; export { getAlertStateRoute } from './get_alert_state'; +export { getAlertStatusRoute } from './get_alert_status'; export { listAlertTypesRoute } from './list_alert_types'; export { updateAlertRoute } from './update'; export { enableAlertRoute } from './enable'; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 4abe58de5a904a..58b1fa4a123e10 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -224,7 +224,7 @@ describe('Task Runner', () => { `); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; - expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.logEvent).toHaveBeenCalledWith({ event: { action: 'execute', @@ -261,6 +261,25 @@ describe('Task Runner', () => { }, message: "test:1: 'alert-name' created new instance: '1'", }); + expect(eventLogger.logEvent).toHaveBeenCalledWith({ + event: { + action: 'active-instance', + }, + kibana: { + alerting: { + instance_id: '1', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "test:1: 'alert-name' active instance: '1'", + }); expect(eventLogger.logEvent).toHaveBeenCalledWith({ event: { action: 'execute-action', @@ -345,7 +364,7 @@ describe('Task Runner', () => { `); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; - expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -388,6 +407,27 @@ describe('Task Runner', () => { "message": "test:1: 'alert-name' created new instance: '1'", }, ], + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1'", + }, + ], Array [ Object { "event": Object { @@ -465,7 +505,7 @@ describe('Task Runner', () => { `); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; - expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -508,6 +548,27 @@ describe('Task Runner', () => { "message": "test:1: 'alert-name' resolved instance: '2'", }, ], + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1'", + }, + ], ] `); }); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 04fea58f250a33..4c16d23b485b5a 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -355,41 +355,53 @@ interface GenerateNewAndResolvedInstanceEventsParams { } function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { - const { currentAlertInstanceIds, originalAlertInstanceIds } = params; + const { + eventLogger, + alertId, + namespace, + currentAlertInstanceIds, + originalAlertInstanceIds, + } = params; + const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); + for (const id of resolvedIds) { + const message = `${params.alertLabel} resolved instance: '${id}'`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message); + } + for (const id of newIds) { const message = `${params.alertLabel} created new instance: '${id}'`; logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message); } - for (const id of resolvedIds) { - const message = `${params.alertLabel} resolved instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message); + for (const id of currentAlertInstanceIds) { + const message = `${params.alertLabel} active instance: '${id}'`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message); } - function logInstanceEvent(id: string, action: string, message: string) { + function logInstanceEvent(instanceId: string, action: string, message: string) { const event: IEvent = { event: { action, }, kibana: { alerting: { - instance_id: id, + instance_id: instanceId, }, saved_objects: [ { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', - id: params.alertId, - namespace: params.namespace, + id: alertId, + namespace, }, ], }, message, }; - params.eventLogger.logEvent(event); + eventLogger.logEvent(event); } } diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index cbdc168a8ffdeb..0a5b169e87d4d3 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -28,6 +28,14 @@ describe('EventLogClientService', () => { eventLogStartService.getClient(request); + const savedObjectGetter = savedObjectProviderRegistry.getProvidersClient(request); + expect(jest.requireMock('./event_log_client').EventLogClient).toHaveBeenCalledWith({ + esContext, + request, + savedObjectGetter, + spacesService: undefined, + }); + expect(savedObjectProviderRegistry.getProvidersClient).toHaveBeenCalledWith(request); }); }); diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index 25b1b95831b8a3..7169aa6ff9baa8 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -14,7 +14,10 @@ export { IEventLogClientService, IEvent, IValidatedEvent, + IEventLogClient, + QueryEventsBySavedObjectResult, SAVED_OBJECT_REL_PRIMARY, } from './types'; + export const config = { schema: ConfigSchema }; export const plugin = (context: PluginInitializerContext) => new Plugin(context); diff --git a/x-pack/plugins/event_log/server/mocks.ts b/x-pack/plugins/event_log/server/mocks.ts index 2f632a52d2f360..39ec9c42522dcc 100644 --- a/x-pack/plugins/event_log/server/mocks.ts +++ b/x-pack/plugins/event_log/server/mocks.ts @@ -7,6 +7,8 @@ import { eventLogServiceMock } from './event_log_service.mock'; import { eventLogStartServiceMock } from './event_log_start_service.mock'; +export { eventLogClientMock } from './event_log_client.mock'; + export { eventLogServiceMock, eventLogStartServiceMock }; export { eventLoggerMock } from './event_logger.mock'; diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index cda95792206234..66030ee3910dc2 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -12,6 +12,7 @@ export { IEvent, IValidatedEvent, EventSchema, ECS_VERSION } from '../generated/ import { IEvent } from '../generated/schemas'; import { FindOptionsType } from './event_log_client'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; +export { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; import { SavedObjectProvider } from './saved_object_provider_registry'; export const SAVED_OBJECT_REL_PRIMARY = 'primary'; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 99d69602db1376..636082656f1a49 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -74,6 +74,7 @@ describe(`feature_privilege_builder`, () => { Array [ "alerting:1.0.0-zeta1:alert-type/my-feature/get", "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus", "alerting:1.0.0-zeta1:alert-type/my-feature/find", ] `); @@ -110,6 +111,7 @@ describe(`feature_privilege_builder`, () => { Array [ "alerting:1.0.0-zeta1:alert-type/my-feature/get", "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus", "alerting:1.0.0-zeta1:alert-type/my-feature/find", "alerting:1.0.0-zeta1:alert-type/my-feature/create", "alerting:1.0.0-zeta1:alert-type/my-feature/delete", @@ -156,6 +158,7 @@ describe(`feature_privilege_builder`, () => { Array [ "alerting:1.0.0-zeta1:alert-type/my-feature/get", "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus", "alerting:1.0.0-zeta1:alert-type/my-feature/find", "alerting:1.0.0-zeta1:alert-type/my-feature/create", "alerting:1.0.0-zeta1:alert-type/my-feature/delete", @@ -169,6 +172,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertStatus", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find", ] `); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 42dd7794ba184f..540b9e5c1e56eb 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -8,7 +8,7 @@ import { uniq } from 'lodash'; import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; -const readOperations: string[] = ['get', 'getAlertState', 'find']; +const readOperations: string[] = ['get', 'getAlertState', 'getAlertStatus', 'find']; const writeOperations: string[] = [ 'create', 'delete', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts index 35fdc3974a2962..7dde344d06fb58 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts @@ -11,7 +11,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pick } from 'lodash'; import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerts/common'; import { BASE_ALERT_API_PATH } from '../constants'; -import { Alert, AlertType, AlertWithoutId, AlertTaskState } from '../../types'; +import { Alert, AlertType, AlertWithoutId, AlertTaskState, AlertStatus } from '../../types'; export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`); @@ -48,6 +48,16 @@ export async function loadAlertState({ }); } +export async function loadAlertStatus({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + return await http.get(`${BASE_ALERT_API_PATH}/alert/${alertId}/status`); +} + export async function loadAlerts({ http, page, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx index dd2ee48b7a6206..ff9b518a9f5b1d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertInstances, AlertInstanceListItem, alertInstanceToListItem } from './alert_instances'; -import { Alert, AlertTaskState, RawAlertInstance } from '../../../../types'; +import { Alert, AlertStatus, AlertInstanceStatus } from '../../../../types'; import { EuiBasicTable } from '@elastic/eui'; const fakeNow = new Date('2020-02-09T23:15:41.941Z'); @@ -34,26 +34,37 @@ jest.mock('../../../app_context', () => { describe('alert_instances', () => { it('render a list of alert instances', () => { const alert = mockAlert(); + const alertStatus = mockAlertStatus({ + instances: { + first_instance: { + status: 'OK', + muted: false, + }, + second_instance: { + status: 'OK', + muted: false, + }, + }, + }); - const alertState = mockAlertState(); const instances: AlertInstanceListItem[] = [ alertInstanceToListItem( fakeNow.getTime(), alert, 'first_instance', - alertState.alertInstances!.first_instance + alertStatus.instances.first_instance ), alertInstanceToListItem( fakeNow.getTime(), alert, 'second_instance', - alertState.alertInstances!.second_instance + alertStatus.instances.second_instance ), ]; expect( shallow( - + ) .find(EuiBasicTable) .prop('items') @@ -62,7 +73,7 @@ describe('alert_instances', () => { it('render a hidden field with duration epoch', () => { const alert = mockAlert(); - const alertState = mockAlertState(); + const alertStatus = mockAlertStatus(); expect( shallow( @@ -71,7 +82,7 @@ describe('alert_instances', () => { {...mockAPIs} alert={alert} readOnly={false} - alertState={alertState} + alertStatus={alertStatus} /> ) .find('[name="alertInstancesDurationEpoch"]') @@ -81,17 +92,15 @@ describe('alert_instances', () => { it('render all active alert instances', () => { const alert = mockAlert(); - const instances = { + const instances: Record = { ['us-central']: { - state: {}, - meta: { - lastScheduledActions: { - group: 'warning', - date: fake2MinutesAgo, - }, - }, + status: 'OK', + muted: false, + }, + ['us-east']: { + status: 'OK', + muted: false, }, - ['us-east']: {}, }; expect( shallow( @@ -99,8 +108,8 @@ describe('alert_instances', () => { {...mockAPIs} alert={alert} readOnly={false} - alertState={mockAlertState({ - alertInstances: instances, + alertStatus={mockAlertStatus({ + instances, })} /> ) @@ -116,6 +125,8 @@ describe('alert_instances', () => { const alert = mockAlert({ mutedInstanceIds: ['us-west', 'us-east'], }); + const instanceUsWest: AlertInstanceStatus = { status: 'OK', muted: false }; + const instanceUsEast: AlertInstanceStatus = { status: 'OK', muted: false }; expect( shallow( @@ -123,16 +134,25 @@ describe('alert_instances', () => { {...mockAPIs} alert={alert} readOnly={false} - alertState={mockAlertState({ - alertInstances: {}, + alertStatus={mockAlertStatus({ + instances: { + 'us-west': { + status: 'OK', + muted: false, + }, + 'us-east': { + status: 'OK', + muted: false, + }, + }, })} /> ) .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-west'), - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east'), + alertInstanceToListItem(fakeNow.getTime(), alert, 'us-west', instanceUsWest), + alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east', instanceUsEast), ]); }); }); @@ -141,13 +161,10 @@ describe('alertInstanceToListItem', () => { it('handles active instances', () => { const alert = mockAlert(); const start = fake2MinutesAgo; - const instance: RawAlertInstance = { - meta: { - lastScheduledActions: { - date: start, - group: 'default', - }, - }, + const instance: AlertInstanceStatus = { + status: 'Active', + muted: false, + activeStartDate: fake2MinutesAgo.toISOString(), }; expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ @@ -164,13 +181,10 @@ describe('alertInstanceToListItem', () => { mutedInstanceIds: ['id'], }); const start = fake2MinutesAgo; - const instance: RawAlertInstance = { - meta: { - lastScheduledActions: { - date: start, - group: 'default', - }, - }, + const instance: AlertInstanceStatus = { + status: 'Active', + muted: true, + activeStartDate: fake2MinutesAgo.toISOString(), }; expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ @@ -182,23 +196,11 @@ describe('alertInstanceToListItem', () => { }); }); - it('handles active instances with no meta', () => { + it('handles active instances with start date', () => { const alert = mockAlert(); - const instance: RawAlertInstance = {}; - - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ - instance: 'id', - status: { label: 'Active', healthColor: 'primary' }, - start: undefined, - duration: 0, - isMuted: false, - }); - }); - - it('handles active instances with no lastScheduledActions', () => { - const alert = mockAlert(); - const instance: RawAlertInstance = { - meta: {}, + const instance: AlertInstanceStatus = { + status: 'Active', + muted: false, }; expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ @@ -214,9 +216,13 @@ describe('alertInstanceToListItem', () => { const alert = mockAlert({ mutedInstanceIds: ['id'], }); - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id')).toEqual({ + const instance: AlertInstanceStatus = { + status: 'OK', + muted: true, + }; + expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'Inactive', healthColor: 'subdued' }, + status: { label: 'OK', healthColor: 'subdued' }, start: undefined, duration: 0, isMuted: true, @@ -247,23 +253,26 @@ function mockAlert(overloads: Partial = {}): Alert { }; } -function mockAlertState(overloads: Partial = {}): AlertTaskState { - return { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: new Date(), - }, - }, +function mockAlertStatus(overloads: Partial = {}): AlertStatus { + const status: AlertStatus = { + id: 'alert-id', + name: 'alert-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: 'alert-type-id', + consumer: 'alert-consumer', + status: 'OK', + muteAll: false, + throttle: '', + enabled: true, + errorMessages: [], + statusStartDate: fake2MinutesAgo.toISOString(), + statusEndDate: fakeNow.toISOString(), + instances: { + foo: { + status: 'OK', + muted: false, }, - second_instance: {}, }, - ...overloads, }; + return { ...status, ...overloads }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index e2391886591780..77a3b454a1820c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch } from '@elastic/eui'; // @ts-ignore import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; -import { padStart, difference, chunk } from 'lodash'; -import { Alert, AlertTaskState, RawAlertInstance, Pagination } from '../../../../types'; +import { padStart, chunk } from 'lodash'; +import { Alert, AlertStatus, AlertInstanceStatus, Pagination } from '../../../../types'; import { ComponentOpts as AlertApis, withBulkAlertOperations, @@ -21,7 +21,7 @@ import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; type AlertInstancesProps = { alert: Alert; readOnly: boolean; - alertState: AlertTaskState; + alertStatus: AlertStatus; requestRefresh: () => Promise; durationEpoch?: number; } & Pick; @@ -113,7 +113,7 @@ function durationAsString(duration: Duration): string { export function AlertInstances({ alert, readOnly, - alertState: { alertInstances = {} }, + alertStatus, muteAlertInstance, unmuteAlertInstance, requestRefresh, @@ -124,15 +124,10 @@ export function AlertInstances({ size: DEFAULT_SEARCH_PAGE_SIZE, }); - const mergedAlertInstances = [ - ...Object.entries(alertInstances).map(([instanceId, instance]) => - alertInstanceToListItem(durationEpoch, alert, instanceId, instance) - ), - ...difference(alert.mutedInstanceIds, Object.keys(alertInstances)).map((instanceId) => - alertInstanceToListItem(durationEpoch, alert, instanceId) - ), - ]; - const pageOfAlertInstances = getPage(mergedAlertInstances, pagination); + const alertInstances = Object.entries(alertStatus.instances).map(([instanceId, instance]) => + alertInstanceToListItem(durationEpoch, alert, instanceId, instance) + ); + const pageOfAlertInstances = getPage(alertInstances, pagination); const onMuteAction = async (instance: AlertInstanceListItem) => { await (instance.isMuted @@ -155,7 +150,7 @@ export function AlertInstances({ pagination={{ pageIndex: pagination.index, pageSize: pagination.size, - totalItemCount: mergedAlertInstances.length, + totalItemCount: alertInstances.length, }} onChange={({ page: changedPage }: { page: Pagination }) => { setPagination(changedPage); @@ -197,29 +192,27 @@ const ACTIVE_LABEL = i18n.translate( const INACTIVE_LABEL = i18n.translate( 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive', - { defaultMessage: 'Inactive' } + { defaultMessage: 'OK' } ); -const durationSince = (durationEpoch: number, startTime?: number) => - startTime ? durationEpoch - startTime : 0; - export function alertInstanceToListItem( durationEpoch: number, alert: Alert, instanceId: string, - instance?: RawAlertInstance + instance: AlertInstanceStatus ): AlertInstanceListItem { - const isMuted = alert.mutedInstanceIds.findIndex((muted) => muted === instanceId) >= 0; + const isMuted = !!instance?.muted; + const status = + instance?.status === 'Active' + ? { label: ACTIVE_LABEL, healthColor: 'primary' } + : { label: INACTIVE_LABEL, healthColor: 'subdued' }; + const start = instance?.activeStartDate ? new Date(instance.activeStartDate) : undefined; + const duration = start ? durationEpoch - start.valueOf() : 0; return { instance: instanceId, - status: instance - ? { label: ACTIVE_LABEL, healthColor: 'primary' } - : { label: INACTIVE_LABEL, healthColor: 'subdued' }, - start: instance?.meta?.lastScheduledActions?.date, - duration: durationSince( - durationEpoch, - instance?.meta?.lastScheduledActions?.date?.getTime() ?? 0 - ), + status, + start, + duration, isMuted, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx index 975856beba556f..61af8f54785219 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx @@ -7,17 +7,20 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { ToastsApi } from 'kibana/public'; -import { AlertInstancesRoute, getAlertState } from './alert_instances_route'; -import { Alert } from '../../../../types'; +import { AlertInstancesRoute, getAlertStatus } from './alert_instances_route'; +import { Alert, AlertStatus } from '../../../../types'; import { EuiLoadingSpinner } from '@elastic/eui'; +const fakeNow = new Date('2020-02-09T23:15:41.941Z'); +const fake2MinutesAgo = new Date('2020-02-09T23:13:41.941Z'); + jest.mock('../../../app_context', () => { const toastNotifications = jest.fn(); return { useAppDependencies: jest.fn(() => ({ toastNotifications })), }; }); -describe('alert_state_route', () => { +describe('alert_status_route', () => { it('render a loader while fetching data', () => { const alert = mockAlert(); @@ -34,25 +37,25 @@ describe('getAlertState useEffect handler', () => { jest.clearAllMocks(); }); - it('fetches alert state', async () => { + it('fetches alert status', async () => { const alert = mockAlert(); - const alertState = mockAlertState(); - const { loadAlertState } = mockApis(); - const { setAlertState } = mockStateSetter(); + const alertStatus = mockAlertStatus(); + const { loadAlertStatus } = mockApis(); + const { setAlertStatus } = mockStateSetter(); - loadAlertState.mockImplementationOnce(async () => alertState); + loadAlertStatus.mockImplementationOnce(async () => alertStatus); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications); + await getAlertStatus(alert.id, loadAlertStatus, setAlertStatus, toastNotifications); - expect(loadAlertState).toHaveBeenCalledWith(alert.id); - expect(setAlertState).toHaveBeenCalledWith(alertState); + expect(loadAlertStatus).toHaveBeenCalledWith(alert.id); + expect(setAlertStatus).toHaveBeenCalledWith(alertStatus); }); - it('displays an error if the alert state isnt found', async () => { + it('displays an error if the alert status isnt found', async () => { const actionType = { id: '.server-log', name: 'Server log', @@ -69,34 +72,34 @@ describe('getAlertState useEffect handler', () => { ], }); - const { loadAlertState } = mockApis(); - const { setAlertState } = mockStateSetter(); + const { loadAlertStatus } = mockApis(); + const { setAlertStatus } = mockStateSetter(); - loadAlertState.mockImplementation(async () => { + loadAlertStatus.mockImplementation(async () => { throw new Error('OMG'); }); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications); + await getAlertStatus(alert.id, loadAlertStatus, setAlertStatus, toastNotifications); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ - title: 'Unable to load alert state: OMG', + title: 'Unable to load alert status: OMG', }); }); }); function mockApis() { return { - loadAlertState: jest.fn(), + loadAlertStatus: jest.fn(), requestRefresh: jest.fn(), }; } function mockStateSetter() { return { - setAlertState: jest.fn(), + setAlertStatus: jest.fn(), }; } @@ -123,22 +126,26 @@ function mockAlert(overloads: Partial = {}): Alert { }; } -function mockAlertState(overloads: Partial = {}): any { - return { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: new Date(), - }, - }, +function mockAlertStatus(overloads: Partial = {}): any { + const status: AlertStatus = { + id: 'alert-id', + name: 'alert-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: 'alert-type-id', + consumer: 'alert-consumer', + status: 'OK', + muteAll: false, + throttle: null, + enabled: true, + errorMessages: [], + statusStartDate: fake2MinutesAgo.toISOString(), + statusEndDate: fakeNow.toISOString(), + instances: { + foo: { + status: 'OK', + muted: false, }, - second_instance: {}, }, }; + return status; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx index d8a7d18eb87a91..3afec45bcad64e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from 'kibana/public'; import React, { useState, useEffect } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { Alert, AlertTaskState } from '../../../../types'; +import { Alert, AlertStatus } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { ComponentOpts as AlertApis, @@ -16,33 +16,33 @@ import { } from '../../common/components/with_bulk_alert_api_operations'; import { AlertInstancesWithApi as AlertInstances } from './alert_instances'; -type WithAlertStateProps = { +type WithAlertStatusProps = { alert: Alert; readOnly: boolean; requestRefresh: () => Promise; -} & Pick; +} & Pick; -export const AlertInstancesRoute: React.FunctionComponent = ({ +export const AlertInstancesRoute: React.FunctionComponent = ({ alert, readOnly, requestRefresh, - loadAlertState, + loadAlertStatus: loadAlertStatus, }) => { const { toastNotifications } = useAppDependencies(); - const [alertState, setAlertState] = useState(null); + const [alertStatus, setAlertStatus] = useState(null); useEffect(() => { - getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications); + getAlertStatus(alert.id, loadAlertStatus, setAlertStatus, toastNotifications); // eslint-disable-next-line react-hooks/exhaustive-deps }, [alert]); - return alertState ? ( + return alertStatus ? ( ) : (
= ); }; -export async function getAlertState( +export async function getAlertStatus( alertId: string, - loadAlertState: AlertApis['loadAlertState'], - setAlertState: React.Dispatch>, + loadAlertStatus: AlertApis['loadAlertStatus'], + setAlertStatus: React.Dispatch>, toastNotifications: Pick ) { try { - const loadedState = await loadAlertState(alertId); - setAlertState(loadedState); + const loadedStatus = await loadAlertStatus(alertId); + setAlertStatus(loadedStatus); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertStateMessage', { - defaultMessage: 'Unable to load alert state: {message}', + defaultMessage: 'Unable to load alert status: {message}', values: { message: e.message, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 0c6f71120cc2ee..fd8b35a96bdf0b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -6,7 +6,13 @@ import React from 'react'; -import { Alert, AlertType, AlertTaskState, AlertingFrameworkHealth } from '../../../../types'; +import { + Alert, + AlertType, + AlertTaskState, + AlertStatus, + AlertingFrameworkHealth, +} from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { deleteAlerts, @@ -22,6 +28,7 @@ import { unmuteAlertInstance, loadAlert, loadAlertState, + loadAlertStatus, loadAlertTypes, health, } from '../../../lib/alert_api'; @@ -51,6 +58,7 @@ export interface ComponentOpts { }>; loadAlert: (id: Alert['id']) => Promise; loadAlertState: (id: Alert['id']) => Promise; + loadAlertStatus: (id: Alert['id']) => Promise; loadAlertTypes: () => Promise; getHealth: () => Promise; } @@ -119,6 +127,7 @@ export function withBulkAlertOperations( deleteAlert={async (alert: Alert) => deleteAlerts({ http, ids: [alert.id] })} loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} loadAlertState={async (alertId: Alert['id']) => loadAlertState({ http, alertId })} + loadAlertStatus={async (alertId: Alert['id']) => loadAlertStatus({ http, alertId })} loadAlertTypes={async () => loadAlertTypes({ http })} getHealth={async () => health({ http })} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index a42a9f56a751fc..0c0d99eed4e7b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -12,10 +12,20 @@ import { SanitizedAlert as Alert, AlertAction, AlertTaskState, + AlertStatus, + AlertInstanceStatus, RawAlertInstance, AlertingFrameworkHealth, } from '../../alerts/common'; -export { Alert, AlertAction, AlertTaskState, RawAlertInstance, AlertingFrameworkHealth }; +export { + Alert, + AlertAction, + AlertTaskState, + AlertStatus, + AlertInstanceStatus, + RawAlertInstance, + AlertingFrameworkHealth, +}; export { ActionType }; export type ActionTypeIndex = Record; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 269a9d3a504a2b..40b2c33a702aa4 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -310,23 +310,31 @@ export function defineAlertTypes( defaultActionGroupId: 'default', async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, state, params } = alertExecutorOptions; - const pattern = params.pattern; - if (!Array.isArray(pattern)) throw new Error('pattern is not an array'); - if (pattern.length === 0) throw new Error('pattern is empty'); + const pattern = params.pattern as Record; + if (typeof pattern !== 'object') throw new Error('pattern is not an object'); + let maxPatternLength = 0; + for (const [instanceId, instancePattern] of Object.entries(pattern)) { + if (!Array.isArray(instancePattern)) { + throw new Error(`pattern for instance ${instanceId} is not an array`); + } + maxPatternLength = Math.max(maxPatternLength, instancePattern.length); + } // get the pattern index, return if past it const patternIndex = state.patternIndex ?? 0; - if (patternIndex > pattern.length) { + if (patternIndex >= maxPatternLength) { return { patternIndex }; } // fire if pattern says to - if (pattern[patternIndex]) { - services.alertInstanceFactory('instance').scheduleActions('default'); + for (const [instanceId, instancePattern] of Object.entries(pattern)) { + if (instancePattern[patternIndex]) { + services.alertInstanceFactory(instanceId).scheduleActions('default'); + } } return { - patternIndex: (patternIndex + 1) % pattern.length, + patternIndex: patternIndex + 1, }; }, }; diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts index 99f51ff244546e..aebcd854514b28 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts @@ -25,7 +25,7 @@ export async function getEventLog(params: GetEventLogParams): Promise { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + describe(scenario.id, () => { + it('should handle getAlertStatus alert request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/status`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + statusCode: 403, + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + const { id, statusStartDate, statusEndDate } = response.body; + expect(id).to.equal(createdAlert.id); + expect(Date.parse(statusStartDate)).to.be.lessThan(Date.parse(statusEndDate)); + + const stableBody = omit(response.body, [ + 'id', + 'statusStartDate', + 'statusEndDate', + 'lastRun', + ]); + expect(stableBody).to.eql({ + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.noop', + consumer: 'alertsFixture', + status: 'OK', + muteAll: false, + throttle: '1m', + enabled: true, + errorMessages: [], + instances: {}, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle getAlertStatus alert request appropriately when unauthorized', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/status`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'get', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'get', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('id', 'instances', 'errorMessages'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`shouldn't getAlertStatus for an alert from another space`, async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}/status`) + .auth(user.username, user.password); + + expect(response.statusCode).to.eql(404); + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + case 'global_read at space1': + case 'superuser at space1': + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`should handle getAlertStatus request appropriately when alert doesn't exist`, async () => { + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/1/status`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 4cd5f0805121c2..45fa075a65978a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -16,6 +16,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./enable')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./get_alert_state')); + loadTestFile(require.resolve('./get_alert_status')); loadTestFile(require.resolve('./list_alert_types')); loadTestFile(require.resolve('./mute_all')); loadTestFile(require.resolve('./mute_instance')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 79d25d8d10436d..a5dff437283aec 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -35,7 +35,9 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .expect(200); // pattern of when the alert should fire - const pattern = [false, true, true]; + const pattern = { + instance: [false, true, true], + }; const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) @@ -70,7 +72,13 @@ export default function eventLogTests({ getService }: FtrProviderContext) { type: 'alert', id: alertId, provider: 'alerting', - actions: ['execute', 'execute-action', 'new-instance', 'resolved-instance'], + actions: [ + 'execute', + 'execute-action', + 'new-instance', + 'active-instance', + 'resolved-instance', + ], }); }); @@ -120,24 +128,27 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], - message: `test.patternFiring:${alertId}: 'abc' created new instance: 'instance'`, - }); + validateInstanceEvent(event, `created new instance: 'instance'`); break; case 'resolved-instance': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], - message: `test.patternFiring:${alertId}: 'abc' resolved instance: 'instance'`, - }); + validateInstanceEvent(event, `resolved instance: 'instance'`); + break; + case 'active-instance': + validateInstanceEvent(event, `active instance: 'instance'`); break; // this will get triggered as we add new event actions default: throw new Error(`unexpected event action "${event?.event?.action}"`); } } + + function validateInstanceEvent(event: IValidatedEvent, subMessage: string) { + validateEvent(event, { + spaceId: Spaces.space1.id, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], + message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, + }); + } }); it('should generate events for execution errors', async () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_status.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_status.ts new file mode 100644 index 00000000000000..341313ce55c60f --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_status.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { omit } from 'lodash'; + +import { Spaces } from '../../scenarios'; +import { + getUrlPrefix, + ObjectRemover, + getTestAlertData, + AlertUtils, + getEventLog, +} from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetAlertStatusTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const retry = getService('retry'); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + describe('getAlertStatus', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + it(`handles non-existant alert`, async () => { + await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/1/status`) + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + }); + + it('handles no-op alert', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + + await waitForEvents(createdAlert.id, ['execute']); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/status` + ); + + expect(response.status).to.eql(200); + + const { statusStartDate, statusEndDate } = response.body; + expect(Date.parse(statusStartDate)).to.be.lessThan(Date.parse(statusEndDate)); + + const stableBody = omit(response.body, ['statusStartDate', 'statusEndDate', 'lastRun']); + expect(stableBody).to.eql({ + id: createdAlert.id, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.noop', + consumer: 'alertsFixture', + status: 'OK', + muteAll: false, + throttle: '1m', + enabled: true, + errorMessages: [], + instances: {}, + }); + }); + + it('handles no-op alert without waiting for execution event', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/status` + ); + + expect(response.status).to.eql(200); + + const { statusStartDate, statusEndDate } = response.body; + expect(Date.parse(statusStartDate)).to.be.lessThan(Date.parse(statusEndDate)); + + const stableBody = omit(response.body, ['statusStartDate', 'statusEndDate', 'lastRun']); + expect(stableBody).to.eql({ + id: createdAlert.id, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.noop', + consumer: 'alertsFixture', + status: 'OK', + muteAll: false, + throttle: '1m', + enabled: true, + errorMessages: [], + instances: {}, + }); + }); + + it('handles dateStart parameter', async () => { + const dateStart = '2020-08-08T08:08:08.008Z'; + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + + await waitForEvents(createdAlert.id, ['execute']); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${ + createdAlert.id + }/status?dateStart=${dateStart}` + ); + expect(response.status).to.eql(200); + const { statusStartDate, statusEndDate } = response.body; + expect(Date.parse(statusStartDate)).to.be.lessThan(Date.parse(statusEndDate)); + expect(statusStartDate).to.be(dateStart); + }); + + it('handles invalid dateStart parameter', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + + await waitForEvents(createdAlert.id, ['execute']); + const dateStart = 'X0X0-08-08T08:08:08.008Z'; + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${ + createdAlert.id + }/status?dateStart=${dateStart}` + ); + expect(response.status).to.eql(400); + expect(response.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'Invalid date for parameter dateStart: "X0X0-08-08T08:08:08.008Z"', + }); + }); + + it('handles muted instances', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + + await alertUtils.muteInstance(createdAlert.id, '1'); + await waitForEvents(createdAlert.id, ['execute']); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/status` + ); + + expect(response.status).to.eql(200); + expect(response.body.instances).to.eql({ + '1': { + status: 'OK', + muted: true, + }, + }); + }); + + it('handles alert errors', async () => { + const dateNow = Date.now(); + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ alertTypeId: 'test.throw' })) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + + await waitForEvents(createdAlert.id, ['execute']); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/status` + ); + const { errorMessages } = response.body; + expect(errorMessages.length).to.be.greaterThan(0); + const errorMessage = errorMessages[0]; + expect(Date.parse(errorMessage.date)).to.be.greaterThan(dateNow); + expect(errorMessage.message).to.be('this alert is intended to fail'); + }); + + it('handles multi-instance status', async () => { + // pattern of when the alert should fire + const pattern = { + instanceA: [true, true, true, true], + instanceB: [true, true, false, false], + instanceC: [true, true, true, true], + }; + + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.patternFiring', + params: { pattern }, + schedule: { interval: '1s' }, + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + + await alertUtils.muteInstance(createdAlert.id, 'instanceC'); + await alertUtils.muteInstance(createdAlert.id, 'instanceD'); + await waitForEvents(createdAlert.id, ['new-instance', 'resolved-instance']); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/status` + ); + + const actualInstances = response.body.instances; + const expectedInstances = { + instanceA: { + status: 'Active', + muted: false, + activeStartDate: actualInstances.instanceA.activeStartDate, + }, + instanceB: { + status: 'OK', + muted: false, + }, + instanceC: { + status: 'Active', + muted: true, + activeStartDate: actualInstances.instanceC.activeStartDate, + }, + instanceD: { + status: 'OK', + muted: true, + }, + }; + expect(actualInstances).to.eql(expectedInstances); + }); + }); + + async function waitForEvents(id: string, actions: string[]) { + await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id, + provider: 'alerting', + actions, + }); + }); + } +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index a23f0fa8353133..b927b563eb54a5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -16,6 +16,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./get_alert_state')); + loadTestFile(require.resolve('./get_alert_status')); loadTestFile(require.resolve('./list_alert_types')); loadTestFile(require.resolve('./event_log')); loadTestFile(require.resolve('./mute_all')); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index d86d272c1da8c8..1579d041c9f583 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -361,7 +361,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // await first run to complete so we have an initial state await retry.try(async () => { - const { alertInstances } = await alerting.alerts.getAlertState(alert.id); + const { instances: alertInstances } = await alerting.alerts.getAlertStatus(alert.id); expect(Object.keys(alertInstances).length).to.eql(instances.length); }); }); @@ -373,15 +373,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertInstancesList'); - const { alertInstances } = await alerting.alerts.getAlertState(alert.id); + const status = await alerting.alerts.getAlertStatus(alert.id); const dateOnAllInstancesFromApiResponse = mapValues( - alertInstances, - ({ - meta: { - lastScheduledActions: { date }, - }, - }) => date + status.instances, + (instance) => instance.activeStartDate ); log.debug( @@ -471,7 +467,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ).to.eql([ { instance: 'eu-east', - status: 'Inactive', + status: 'OK', start: '', duration: '', }, @@ -574,7 +570,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // await first run to complete so we have an initial state await retry.try(async () => { - const { alertInstances } = await alerting.alerts.getAlertState(alert.id); + const { instances: alertInstances } = await alerting.alerts.getAlertStatus(alert.id); expect(Object.keys(alertInstances).length).to.eql(instances.length); }); @@ -595,7 +591,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertInstancesList'); - const { alertInstances } = await alerting.alerts.getAlertState(alert.id); + const { instances: alertInstances } = await alerting.alerts.getAlertStatus(alert.id); const items = await pageObjects.alertDetailsUI.getAlertInstancesList(); expect(items.length).to.eql(PAGE_SIZE); @@ -608,7 +604,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertInstancesList'); - const { alertInstances } = await alerting.alerts.getAlertState(alert.id); + const { instances: alertInstances } = await alerting.alerts.getAlertStatus(alert.id); await pageObjects.alertDetailsUI.clickPaginationNextPage(); diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts index 23a4529139c538..c6fbdecf77f16a 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -8,6 +8,21 @@ import axios, { AxiosInstance } from 'axios'; import util from 'util'; import { ToolingLog } from '@kbn/dev-utils'; +export interface AlertStatus { + status: string; + muted: boolean; + enabled: boolean; + lastRun?: string; + errorMessage?: string; + instances: Record; +} + +export interface AlertInstanceStatus { + status: string; + muted: boolean; + activeStartDate?: string; +} + export class Alerts { private log: ToolingLog; private axios: AxiosInstance; @@ -141,10 +156,10 @@ export class Alerts { this.log.debug(`deleted alert ${alert.id}`); } - public async getAlertState(id: string) { + public async getAlertStatus(id: string): Promise { this.log.debug(`getting alert ${id} state`); - const { data } = await this.axios.get(`/api/alerts/alert/${id}/state`); + const { data } = await this.axios.get(`/api/alerts/alert/${id}/status`); return data; } From f6f59ec261d55e77d48b766cc0d3d64033b522d7 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 14 Aug 2020 15:46:55 +0300 Subject: [PATCH 18/46] Drilldowns for TSVB / Vega / Timelion (#74848) * Drilldowns for TSVB / Vega Closes: #60611 * fix PR comment * fix PR comments * add support for Timelion * rename vis.API.events.brush -> vis.API.events.applyFilter --- .../public/components/chart.tsx | 2 + .../public/components/panel.tsx | 22 ++++++++--- .../public/components/timelion_vis.tsx | 1 + .../public/timelion_vis_type.tsx | 5 +++ .../application/components/vis_editor.js | 2 +- ...r.test.js => create_brush_handler.test.ts} | 37 ++++++++++--------- ...ush_handler.js => create_brush_handler.ts} | 25 +++++++++---- .../public/metrics_type.ts | 4 ++ src/plugins/vis_type_vega/public/vega_type.ts | 4 ++ .../public/vega_view/vega_base_view.js | 21 ++++++++++- .../public/vega_visualization.js | 1 + .../public/vega_visualization.test.js | 5 +++ .../public/embeddable/events.ts | 8 +++- .../public/embeddable/visualize_embeddable.ts | 21 ++++++++--- .../visualizations/public/expressions/vis.ts | 5 +++ src/plugins/visualizations/public/index.ts | 1 + 16 files changed, 124 insertions(+), 40 deletions(-) rename src/plugins/vis_type_timeseries/public/application/lib/{create_brush_handler.test.js => create_brush_handler.test.ts} (58%) rename src/plugins/vis_type_timeseries/public/application/lib/{create_brush_handler.js => create_brush_handler.ts} (68%) diff --git a/src/plugins/vis_type_timelion/public/components/chart.tsx b/src/plugins/vis_type_timelion/public/components/chart.tsx index a8b03bdbc8b7e5..15a376d4e96386 100644 --- a/src/plugins/vis_type_timelion/public/components/chart.tsx +++ b/src/plugins/vis_type_timelion/public/components/chart.tsx @@ -21,8 +21,10 @@ import React from 'react'; import { Sheet } from '../helpers/timelion_request_handler'; import { Panel } from './panel'; +import { ExprVisAPIEvents } from '../../../visualizations/public'; interface ChartComponentProp { + applyFilter: ExprVisAPIEvents['applyFilter']; interval: string; renderComplete(): void; seriesList: Sheet; diff --git a/src/plugins/vis_type_timelion/public/components/panel.tsx b/src/plugins/vis_type_timelion/public/components/panel.tsx index f4f1cd84613bea..9c30a6b75d6dbc 100644 --- a/src/plugins/vis_type_timelion/public/components/panel.tsx +++ b/src/plugins/vis_type_timelion/public/components/panel.tsx @@ -33,10 +33,12 @@ import { colors, Axis, } from '../helpers/panel_utils'; + import { Series, Sheet } from '../helpers/timelion_request_handler'; import { tickFormatters } from '../helpers/tick_formatters'; import { generateTicksProvider } from '../helpers/tick_generator'; import { TimelionVisDependencies } from '../plugin'; +import { ExprVisAPIEvents } from '../../../visualizations/public'; interface CrosshairPlot extends jquery.flot.plot { setCrosshair: (pos: Position) => void; @@ -44,6 +46,7 @@ interface CrosshairPlot extends jquery.flot.plot { } interface PanelProps { + applyFilter: ExprVisAPIEvents['applyFilter']; interval: string; seriesList: Sheet; renderComplete(): void; @@ -72,7 +75,7 @@ const DEBOUNCE_DELAY = 50; // ensure legend is the same height with or without a caption so legend items do not move around const emptyCaption = '
'; -function Panel({ interval, seriesList, renderComplete }: PanelProps) { +function Panel({ interval, seriesList, renderComplete, applyFilter }: PanelProps) { const kibana = useKibana(); const [chart, setChart] = useState(() => cloneDeep(seriesList.list)); const [canvasElem, setCanvasElem] = useState(); @@ -346,12 +349,21 @@ function Panel({ interval, seriesList, renderComplete }: PanelProps) { const plotSelectedHandler = useCallback( (event: JQuery.TriggeredEvent, ranges: Ranges) => { - kibana.services.timefilter.setTime({ - from: moment(ranges.xaxis.from), - to: moment(ranges.xaxis.to), + applyFilter({ + timeFieldName: '*', + filters: [ + { + range: { + '*': { + gte: ranges.xaxis.from, + lte: ranges.xaxis.to, + }, + }, + }, + ], }); }, - [kibana.services.timefilter] + [applyFilter] ); useEffect(() => { diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx index 4bb07fe74ee821..aa594c749b600e 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx @@ -38,6 +38,7 @@ function TimelionVisComponent(props: TimelionVisComponentProp) { return (
{ + return [VIS_EVENT_TO_TRIGGER.applyFilter]; + }, options: { showIndexSelection: false, showQueryBar: false, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 300e70f3ae0c00..50585869862eea 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -50,7 +50,7 @@ export class VisEditor extends Component { visFields: props.visFields, extractedIndexPatterns: [''], }; - this.onBrush = createBrushHandler(getDataStart().query.timefilter.timefilter); + this.onBrush = createBrushHandler((data) => props.vis.API.events.applyFilter(data)); this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); this.visData$ = this.visDataSubject.asObservable().pipe(share()); diff --git a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.js b/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.ts similarity index 58% rename from src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.js rename to src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.ts index 6ae01a384e7cae..a9568b5be9d3fa 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.test.ts @@ -18,28 +18,31 @@ */ import { createBrushHandler } from './create_brush_handler'; -import moment from 'moment'; +import { ExprVisAPIEvents } from '../../../../visualizations/public'; describe('brushHandler', () => { - let mockTimefilter; - let onBrush; + let onBrush: ReturnType; + let applyFilter: ExprVisAPIEvents['applyFilter']; beforeEach(() => { - mockTimefilter = { - time: {}, - setTime: function (time) { - this.time = time; - }, - }; - onBrush = createBrushHandler(mockTimefilter); + applyFilter = jest.fn(); + + onBrush = createBrushHandler(applyFilter); }); - it('returns brushHandler() that updates timefilter', () => { - const from = '2017-01-01T00:00:00Z'; - const to = '2017-01-01T00:10:00Z'; - onBrush(from, to); - expect(mockTimefilter.time.from).toEqual(moment(from).toISOString()); - expect(mockTimefilter.time.to).toEqual(moment(to).toISOString()); - expect(mockTimefilter.time.mode).toEqual('absolute'); + test('returns brushHandler() should updates timefilter through vis.API.events.applyFilter', () => { + const gte = '2017-01-01T00:00:00Z'; + const lte = '2017-01-01T00:10:00Z'; + + onBrush(gte, lte); + + expect(applyFilter).toHaveBeenCalledWith({ + timeFieldName: '*', + filters: [ + { + range: { '*': { gte: '2017-01-01T00:00:00Z', lte: '2017-01-01T00:10:00Z' } }, + }, + ], + }); }); }); diff --git a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.js b/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.ts similarity index 68% rename from src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.js rename to src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.ts index 452e85c6405fe8..38002c75529523 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/create_brush_handler.ts @@ -17,14 +17,23 @@ * under the License. */ -import moment from 'moment'; +import { ExprVisAPIEvents } from '../../../../visualizations/public'; -const TIME_MODE = 'absolute'; - -export const createBrushHandler = (timefilter) => (from, to) => { - timefilter.setTime({ - from: moment(from).toISOString(), - to: moment(to).toISOString(), - mode: TIME_MODE, +export const createBrushHandler = (applyFilter: ExprVisAPIEvents['applyFilter']) => ( + gte: string, + lte: string +) => { + return applyFilter({ + timeFieldName: '*', + filters: [ + { + range: { + '*': { + gte, + lte, + }, + }, + }, + ], }); }; diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 44b0334a37871f..d6621870fef67e 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -25,6 +25,7 @@ import { EditorController } from './application'; // @ts-ignore import { PANEL_TYPES } from '../common/panel_types'; import { VisEditor } from './application/components/vis_editor_lazy'; +import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; export const metricsVisDefinition = { name: 'metrics', @@ -78,6 +79,9 @@ export const metricsVisDefinition = { showIndexSelection: false, }, requestHandler: metricsRequestHandler, + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.applyFilter]; + }, inspectorAdapters: {}, responseHandler: 'none', }; diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index d69eb3cfba282d..f49816017b6843 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -27,6 +27,7 @@ import { createVegaRequestHandler } from './vega_request_handler'; import { createVegaVisualization } from './vega_visualization'; import { getDefaultSpec } from './default_spec'; import { createInspectorAdapters } from './vega_inspector'; +import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => { const requestHandler = createVegaRequestHandler(dependencies); @@ -54,6 +55,9 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen showQueryBar: true, showFilterBar: true, }, + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.applyFilter]; + }, stage: 'experimental', inspectorAdapters: createInspectorAdapters, }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 4596b473644942..a2a973d232de08 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -63,6 +63,7 @@ export class VegaBaseView { this._parser = opts.vegaParser; this._serviceSettings = opts.serviceSettings; this._filterManager = opts.filterManager; + this._applyFilter = opts.applyFilter; this._timefilter = opts.timefilter; this._findIndex = opts.findIndex; this._view = null; @@ -263,7 +264,8 @@ export class VegaBaseView { async addFilterHandler(query, index) { const indexId = await this._findIndex(index); const filter = esFilters.buildQueryFilter(query, indexId); - this._filterManager.addFilters(filter); + + this._applyFilter({ filters: [filter] }); } /** @@ -298,7 +300,22 @@ export class VegaBaseView { * @param {number|string|Date} end */ setTimeFilterHandler(start, end) { - this._timefilter.setTime(VegaBaseView._parseTimeRange(start, end)); + const { from, to, mode } = VegaBaseView._parseTimeRange(start, end); + + this._applyFilter({ + timeFieldName: '*', + filters: [ + { + range: { + '*': { + mode, + gte: from, + lte: to, + }, + }, + }, + ], + }); } /** diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js index 1fcb89f04457da..d6db0f9ea239f9 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.js @@ -106,6 +106,7 @@ export const createVegaVisualization = ({ serviceSettings }) => const { timefilter } = this.dataPlugin.query.timefilter; const vegaViewParams = { parentEl: this._el, + applyFilter: this._vis.API.events.applyFilter, vegaParser, serviceSettings, filterManager, diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index 3e318fa22c195e..0912edf9503a6d 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -105,6 +105,11 @@ describe('VegaVisualizations', () => { vis = { type: vegaVisType, + API: { + events: { + applyFilter: jest.fn(), + }, + }, }; }); diff --git a/src/plugins/visualizations/public/embeddable/events.ts b/src/plugins/visualizations/public/embeddable/events.ts index 0957895a214036..52cac59fbffaa4 100644 --- a/src/plugins/visualizations/public/embeddable/events.ts +++ b/src/plugins/visualizations/public/embeddable/events.ts @@ -17,14 +17,20 @@ * under the License. */ -import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER } from '../../../../plugins/ui_actions/public'; +import { + APPLY_FILTER_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../plugins/ui_actions/public'; export interface VisEventToTrigger { + ['applyFilter']: typeof APPLY_FILTER_TRIGGER; ['brush']: typeof SELECT_RANGE_TRIGGER; ['filter']: typeof VALUE_CLICK_TRIGGER; } export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = { + applyFilter: APPLY_FILTER_TRIGGER, brush: SELECT_RANGE_TRIGGER, filter: VALUE_CLICK_TRIGGER, }; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index cc24c0509fbc12..80e577930fa8d2 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -310,12 +310,21 @@ export class VisualizeEmbeddable extends Embeddable void; brush: (data: any) => void; + applyFilter: (data: any) => void; } export interface ExprVisAPI { @@ -83,6 +84,10 @@ export class ExprVis extends EventEmitter { if (!this.eventsSubject) return; this.eventsSubject.next({ name: 'brush', data }); }, + applyFilter: (data: any) => { + if (!this.eventsSubject) return; + this.eventsSubject.next({ name: 'applyFilter', data }); + }, }, }; } diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 2ac53c2c81acc9..49cfbe76aa9d0a 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -51,5 +51,6 @@ export { VisSavedObject, VisResponseValue, } from './types'; +export { ExprVisAPIEvents } from './expressions/vis'; export { VisualizationListItem } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; From 187a13075b5fe03a165be68faa9c31516ab74a28 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 14 Aug 2020 07:15:16 -0600 Subject: [PATCH 19/46] [file upload] lazy load to reduce page load size (#74967) * [file upload] lazy load to reduce page load size * tslint --- .../public/get_file_upload_component.ts | 38 +++++++++++++++++++ x-pack/plugins/file_upload/public/index.ts | 2 + x-pack/plugins/file_upload/public/plugin.ts | 6 +-- .../layers/file_upload_wizard/wizard.tsx | 19 +++++++++- .../plugins/maps/public/kibana_services.d.ts | 4 +- x-pack/plugins/maps/public/kibana_services.js | 4 +- 6 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/file_upload/public/get_file_upload_component.ts diff --git a/x-pack/plugins/file_upload/public/get_file_upload_component.ts b/x-pack/plugins/file_upload/public/get_file_upload_component.ts new file mode 100644 index 00000000000000..7232c4126e297f --- /dev/null +++ b/x-pack/plugins/file_upload/public/get_file_upload_component.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FeatureCollection } from 'geojson'; + +export interface FileUploadComponentProps { + appName: string; + isIndexingTriggered: boolean; + onFileUpload: (geojsonFile: FeatureCollection, name: string) => void; + onFileRemove: () => void; + onIndexReady: (indexReady: boolean) => void; + transformDetails: string; + onIndexingComplete: (indexResponses: { + indexDataResp: unknown; + indexPatternResp: unknown; + }) => void; +} + +let lazyLoadPromise: Promise>; + +export async function getFileUploadComponent(): Promise< + React.ComponentType +> { + if (typeof lazyLoadPromise !== 'undefined') { + return lazyLoadPromise; + } + + lazyLoadPromise = new Promise(async (resolve) => { + // @ts-expect-error + const { JsonUploadAndParse } = await import('./components/json_upload_and_parse'); + resolve(JsonUploadAndParse); + }); + return lazyLoadPromise; +} diff --git a/x-pack/plugins/file_upload/public/index.ts b/x-pack/plugins/file_upload/public/index.ts index 205ceae37d6a11..1e39fb4dc85960 100644 --- a/x-pack/plugins/file_upload/public/index.ts +++ b/x-pack/plugins/file_upload/public/index.ts @@ -9,3 +9,5 @@ import { FileUploadPlugin } from './plugin'; export function plugin() { return new FileUploadPlugin(); } + +export { FileUploadComponentProps } from './get_file_upload_component'; diff --git a/x-pack/plugins/file_upload/public/plugin.ts b/x-pack/plugins/file_upload/public/plugin.ts index 338c61ad141c62..ff74be659aecaa 100644 --- a/x-pack/plugins/file_upload/public/plugin.ts +++ b/x-pack/plugins/file_upload/public/plugin.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; -// @ts-ignore -import { JsonUploadAndParse } from './components/json_upload_and_parse'; +import { getFileUploadComponent } from './get_file_upload_component'; // @ts-ignore import { setupInitServicesAndConstants, startInitServicesAndConstants } from './kibana_services'; import { IDataPluginServices } from '../../../../src/plugins/data/public'; @@ -35,7 +33,7 @@ export class FileUploadPlugin implements Plugin | null; } export class ClientFileCreateSourceEditor extends Component { private _isMounted: boolean = false; - state = { + state: State = { indexingStage: null, + fileUploadComponent: null, }; componentDidMount() { this._isMounted = true; + this._loadFileUploadComponent(); } componentWillUnmount() { @@ -59,6 +63,13 @@ export class ClientFileCreateSourceEditor extends Component { if (!this._isMounted) { return; @@ -145,7 +156,11 @@ export class ClientFileCreateSourceEditor extends Component >; export function getIndexPatternSelectComponent(): any; export function getHttp(): any; export function getTimeFilter(): any; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 9b035a87a3b378..64aa0e07ffafb2 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -33,8 +33,8 @@ export const getInspector = () => { let fileUploadPlugin; export const setFileUpload = (fileUpload) => (fileUploadPlugin = fileUpload); -export const getFileUploadComponent = () => { - return fileUploadPlugin.JsonUploadAndParse; +export const getFileUploadComponent = async () => { + return await fileUploadPlugin.getFileUploadComponent(); }; let uiSettings; From 8aa8b04cee7eceac3fb87ff19d5ab2c12a9c2a26 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Fri, 14 Aug 2020 10:06:51 -0400 Subject: [PATCH 20/46] [SECURITY_SOLUTION] Retry on ingest setup (#75000) --- x-pack/test/common/services/ingest_manager.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/test/common/services/ingest_manager.ts b/x-pack/test/common/services/ingest_manager.ts index 96b1b97a68dc93..2fcfaa014b2e12 100644 --- a/x-pack/test/common/services/ingest_manager.ts +++ b/x-pack/test/common/services/ingest_manager.ts @@ -8,15 +8,18 @@ import { fleetSetupRouteService } from '../../../plugins/ingest_manager/common'; export function IngestManagerProvider({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const retry = getService('retry'); return { async setup() { const headers = { accept: 'application/json', 'kbn-xsrf': 'some-xsrf-token' }; - await supertest - .post(fleetSetupRouteService.postFleetSetupPath()) - .set(headers) - .send({ forceRecreate: true }) - .expect(200); + await retry.try(async () => { + await supertest + .post(fleetSetupRouteService.postFleetSetupPath()) + .set(headers) + .send({ forceRecreate: true }) + .expect(200); + }); }, }; } From ec5112b9cc6dcdb37242407925d6c0f2a10ffe50 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 14 Aug 2020 10:54:43 -0400 Subject: [PATCH 21/46] [Lens] Fix table sorting bug (#74902) * [Lens] Fix table sorting bug * Fix types --- .../visualization.test.tsx | 25 +++++++++++++++++++ .../datatable_visualization/visualization.tsx | 8 ++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index e18190b6c2d692..0b6584277ffa77 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Ast } from '@kbn/interpreter/common'; +import { buildExpression } from '../../../../../src/plugins/expressions/public'; import { createMockDatasource } from '../editor_frame_service/mocks'; import { DatatableVisualizationState, datatableVisualization } from './visualization'; import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; @@ -324,4 +326,27 @@ describe('Datatable Visualization', () => { }); }); }); + + describe('#toExpression', () => { + it('reorders the rendered colums based on the order from the datasource', () => { + const datasource = createMockDatasource('test'); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + isBucketed: true, + label: 'label', + }); + + const expression = datatableVisualization.toExpression({ layers: [layer] }, frame) as Ast; + const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns'); + + expect(tableArgs).toHaveLength(1); + expect(tableArgs[0].arguments).toEqual({ + columnIds: ['c', 'b'], + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index e4b371143594a2..659f8ea12bcb04 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; import chartTableSVG from '../assets/chart_datatable.svg'; @@ -185,10 +186,13 @@ export const datatableVisualization: Visualization< }; }, - toExpression(state, frame) { + toExpression(state, frame): Ast { const layer = state.layers[0]; const datasource = frame.datasourceLayers[layer.layerId]; - const operations = layer.columns + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + const operations = sortedColumns .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); From 458bf9fb0db04a7648f9466520a6bdae2004e2b3 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 14 Aug 2020 17:57:08 +0300 Subject: [PATCH 22/46] Fix bug on TopN weird behavior with zero values (#74942) --- .../public/application/visualizations/views/top_n.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js index b595979130d3a8..3aae1bd64d9533 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js @@ -110,7 +110,9 @@ export class TopN extends Component { const isPositiveValue = lastValue >= 0; const intervalLength = TopN.calcDomain(renderMode, min, max); - const width = 100 * (Math.abs(lastValue) / intervalLength); + // if both are 0, the division returns NaN causing unexpected behavior. + // For this it defaults to 0 + const width = 100 * (Math.abs(lastValue) / intervalLength) || 0; const styles = reactcss( { From 344e1de7e1a0962c4e4c1ebf2ed81fccebf84b15 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 14 Aug 2020 17:14:03 +0200 Subject: [PATCH 23/46] [Upgrade Assistant] Fix potentially long URI (#74485) * _cluster/state/metadata/* and added a comment to function * add another comment regarding why we are asking for * * update jest test * refactor and clean up use of cluster status to get index state Co-authored-by: Elastic Machine --- .../common/get_index_state.test.ts | 41 +++++++++++++ .../common/get_index_state.ts | 24 ++++++++ ...get_index_state_from_cluster_state.test.ts | 53 ----------------- .../get_index_state_from_cluster_state.ts | 28 --------- .../plugins/upgrade_assistant/common/types.ts | 50 +++++----------- .../server/lib/es_indices_state_check.ts | 21 ++++--- .../server/lib/es_migration_apis.test.ts | 28 ++++----- .../server/lib/es_migration_apis.ts | 2 +- .../server/lib/reindexing/reindex_service.ts | 6 +- .../upgrade_assistant/index.js | 1 - .../upgrade_assistant/reindexing.js | 16 ++--- .../upgrade_assistant/status.ts | 58 ------------------- 12 files changed, 108 insertions(+), 220 deletions(-) create mode 100644 x-pack/plugins/upgrade_assistant/common/get_index_state.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/common/get_index_state.ts delete mode 100644 x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.test.ts delete mode 100644 x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.ts delete mode 100644 x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts diff --git a/x-pack/plugins/upgrade_assistant/common/get_index_state.test.ts b/x-pack/plugins/upgrade_assistant/common/get_index_state.test.ts new file mode 100644 index 00000000000000..43c20add0bb3c4 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/common/get_index_state.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { getIndexState } from './get_index_state'; +import { ResolveIndexResponseFromES } from './types'; + +describe('getIndexState', () => { + const indexName1 = 'indexName'; + const indexName2 = 'indexName2'; + const response: ResolveIndexResponseFromES = { + indices: [ + { + name: indexName2, + aliases: ['.security'], + attributes: ['open'], + }, + { + name: indexName1, + attributes: ['closed'], + }, + ], + aliases: [ + { + name: '.security', + indices: ['.security-7'], + }, + ], + data_streams: [], + }; + + it('correctly extracts state', () => { + expect(getIndexState(indexName1, response)).toBe('closed'); + expect(getIndexState(indexName2, response)).toBe('open'); + }); + + it('throws if the index name cannot be found', () => { + expect(() => getIndexState('nonExistent', response)).toThrow('not found'); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/common/get_index_state.ts b/x-pack/plugins/upgrade_assistant/common/get_index_state.ts new file mode 100644 index 00000000000000..24d044f324d07b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/common/get_index_state.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ResolveIndexResponseFromES } from './types'; + +/** + * Throws if the index name is not found in the resolved indices response + * + * @param indexName Assume this is an index name, not an alias + * @param resolvedResponse The response from _resolve/index/ + */ +export const getIndexState = ( + indexName: string, + resolvedResponse: ResolveIndexResponseFromES +): 'open' | 'closed' => { + const index = resolvedResponse.indices.find((i) => i.name === indexName); + if (index) { + return index.attributes.includes('closed') ? 'closed' : 'open'; + } + throw new Error(`${indexName} not found!`); +}; diff --git a/x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.test.ts b/x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.test.ts deleted file mode 100644 index 1098594a68f8a2..00000000000000 --- a/x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { getIndexStateFromClusterState } from './get_index_state_from_cluster_state'; -import { ClusterStateAPIResponse } from './types'; - -describe('getIndexStateFromClusterState', () => { - const indexName = 'indexName'; - const clusterState: ClusterStateAPIResponse = { - metadata: { - indices: {}, - cluster_coordination: {} as any, - cluster_uuid: 'test', - templates: {} as any, - }, - cluster_name: 'test', - cluster_uuid: 'test', - }; - - afterEach(() => { - clusterState.metadata.indices = {}; - }); - - it('correctly extracts state from cluster state', () => { - clusterState.metadata.indices[indexName] = { state: 'open' } as any; - clusterState.metadata.indices.aTotallyDifferentIndex = { state: 'close' } as any; - expect(getIndexStateFromClusterState(indexName, clusterState)).toBe('open'); - }); - - it('correctly extracts state from aliased index in cluster state', () => { - clusterState.metadata.indices.aTotallyDifferentName = { - state: 'close', - aliases: [indexName, 'test'], - } as any; - clusterState.metadata.indices.aTotallyDifferentName1 = { - state: 'open', - aliases: ['another', 'test'], - } as any; - - expect(getIndexStateFromClusterState(indexName, clusterState)).toBe('close'); - }); - - it('throws if the index name cannot be found in the cluster state', () => { - expect(() => getIndexStateFromClusterState(indexName, clusterState)).toThrow('not found'); - clusterState.metadata.indices.aTotallyDifferentName1 = { - state: 'open', - aliases: ['another', 'test'], - } as any; - expect(() => getIndexStateFromClusterState(indexName, clusterState)).toThrow('not found'); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.ts b/x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.ts deleted file mode 100644 index f302940424ee3f..00000000000000 --- a/x-pack/plugins/upgrade_assistant/common/get_index_state_from_cluster_state.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ClusterStateAPIResponse } from './types'; - -const checkAllAliases = ( - indexName: string, - clusterState: ClusterStateAPIResponse -): 'open' | 'close' => { - for (const index of Object.values(clusterState.metadata.indices)) { - if (index.aliases?.some((alias) => alias === indexName)) { - return index.state; - } - } - - throw new Error(`${indexName} not found in cluster state!`); -}; - -export const getIndexStateFromClusterState = ( - indexName: string, - clusterState: ClusterStateAPIResponse -): 'open' | 'close' => - clusterState.metadata.indices[indexName] - ? clusterState.metadata.indices[indexName].state - : checkAllAliases(indexName, clusterState); diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index 6c1b24b677754a..a29c37c9a988c8 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -184,41 +184,17 @@ export interface UpgradeAssistantStatus { indices: EnrichedDeprecationInfo[]; } -export interface ClusterStateIndexAPIResponse { - state: 'open' | 'close'; - settings: { - index: { - verified_before_close: string; - search: { - throttled: string; - }; - number_of_shards: string; - provided_name: string; - frozen: string; - creation_date: string; - number_of_replicas: string; - uuid: string; - version: { - created: string; - }; - }; - }; - mappings: any; - aliases: string[]; -} - -export interface ClusterStateAPIResponse { - cluster_name: string; - cluster_uuid: string; - metadata: { - cluster_uuid: string; - cluster_coordination: { - term: number; - last_committed_config: string[]; - last_accepted_config: string[]; - voting_config_exclusions: []; - }; - templates: any; - indices: { [indexName: string]: ClusterStateIndexAPIResponse }; - }; +export interface ResolveIndexResponseFromES { + indices: Array<{ + name: string; + // per https://github.com/elastic/elasticsearch/pull/57626 + attributes: Array<'open' | 'closed' | 'hidden' | 'frozen'>; + aliases?: string[]; + data_stream?: string; + }>; + aliases: Array<{ + name: string; + indices: string[]; + }>; + data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>; } diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts index cec89bbe2745e7..bce48b152700f0 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_indices_state_check.ts @@ -5,28 +5,27 @@ */ import { LegacyAPICaller } from 'kibana/server'; -import { getIndexStateFromClusterState } from '../../common/get_index_state_from_cluster_state'; -import { ClusterStateAPIResponse } from '../../common/types'; +import { getIndexState } from '../../common/get_index_state'; +import { ResolveIndexResponseFromES } from '../../common/types'; -type StatusCheckResult = Record; +type StatusCheckResult = Record; export const esIndicesStateCheck = async ( callAsUser: LegacyAPICaller, indices: string[] ): Promise => { - // According to https://www.elastic.co/guide/en/elasticsearch/reference/7.6/cluster-state.html - // The response from this call is considered internal and subject to change. We have an API - // integration test for asserting that the current ES version still returns what we expect. - // This lives in x-pack/test/upgrade_assistant_integration - const clusterState: ClusterStateAPIResponse = await callAsUser('cluster.state', { - index: indices, - metric: 'metadata', + const response: ResolveIndexResponseFromES = await callAsUser('transport.request', { + method: 'GET', + path: `/_resolve/index/*`, + query: { + expand_wildcards: 'all', + }, }); const result: StatusCheckResult = {}; indices.forEach((index) => { - result[index] = getIndexStateFromClusterState(index, clusterState); + result[index] = getIndexState(index, response); }); return result; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index 2a4fa5cd48ded2..6e524a98afdc6b 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -6,33 +6,25 @@ import _ from 'lodash'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import { getUpgradeAssistantStatus } from './es_migration_apis'; - import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; + +import { getUpgradeAssistantStatus } from './es_migration_apis'; import fakeDeprecations from './__fixtures__/fake_deprecations.json'; +const fakeIndexNames = Object.keys(fakeDeprecations.index_settings); + describe('getUpgradeAssistantStatus', () => { + const resolvedIndices = { + indices: fakeIndexNames.map((f) => ({ name: f, attributes: ['open'] })), + }; let deprecationsResponse: DeprecationAPIResponse; const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - (dataClient.callAsCurrentUser as jest.Mock).mockImplementation(async (api, { path, index }) => { + (dataClient.callAsCurrentUser as jest.Mock).mockImplementation(async (api, { path }) => { if (path === '/_migration/deprecations') { return deprecationsResponse; - } else if (api === 'cluster.state') { - return { - metadata: { - indices: { - ...index.reduce((acc: any, i: any) => { - return { - ...acc, - [i]: { - state: 'open', - }, - }; - }, {}), - }, - }, - }; + } else if (path === '/_resolve/index/*') { + return resolvedIndices; } else if (api === 'indices.getMapping') { return {}; } else { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index 98175e2ae791eb..abbeb8a89e12a6 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -34,7 +34,7 @@ export async function getUpgradeAssistantStatus( indices.forEach((indexData) => { indexData.blockerForReindexing = - indexStates[indexData.index!] === 'close' ? 'index-closed' : undefined; + indexStates[indexData.index!] === 'closed' ? 'index-closed' : undefined; }); } diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index a23ddc0d60329e..e784f42867d572 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -346,8 +346,8 @@ export const reindexServiceFactory = ( // Where possible, derive reindex options at the last moment before reindexing // to prevent them from becoming stale as they wait in the queue. const indicesState = await esIndicesStateCheck(callAsUser, [indexName]); - const openAndClose = indicesState[indexName] === 'close'; - if (indicesState[indexName] === 'close') { + const shouldOpenAndClose = indicesState[indexName] === 'closed'; + if (shouldOpenAndClose) { log.debug(`Detected closed index ${indexName}, opening...`); await callAsUser('indices.open', { index: indexName }); } @@ -369,7 +369,7 @@ export const reindexServiceFactory = ( ...(reindexOptions ?? {}), // Indicate to downstream states whether we opened a closed index that should be // closed again. - openAndClose, + openAndClose: shouldOpenAndClose, }, }); }; diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js index 638f31dc211b5e..1b7406b37022a9 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js @@ -9,6 +9,5 @@ export default function ({ loadTestFile }) { this.tags('ciGroup7'); loadTestFile(require.resolve('./reindexing')); - loadTestFile(require.resolve('./status')); }); } diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js index 1131089130d36e..10074f68bb59ea 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { ReindexStatus, REINDEX_OP_TYPE } from '../../../plugins/upgrade_assistant/common/types'; import { generateNewIndexName } from '../../../plugins/upgrade_assistant/server/lib/reindexing/index_settings'; -import { getIndexStateFromClusterState } from '../../../plugins/upgrade_assistant/common/get_index_state_from_cluster_state'; +import { getIndexState } from '../../../plugins/upgrade_assistant/common/get_index_state'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -190,7 +190,7 @@ export default function ({ getService }) { expect(result.body.enqueued.length).to.equal(3); expect(result.body.errors.length).to.equal(0); - const [{ newIndexName: newTest1Name }] = result.body.enqueued; + const [{ newIndexName: nameOfIndexThatShouldBeClosed }] = result.body.enqueued; await assertQueueState(test1, 3); await waitForReindexToComplete(test1); @@ -204,16 +204,12 @@ export default function ({ getService }) { await assertQueueState(undefined, 0); // Check that the closed index is still closed after reindexing - const clusterStateResponse = await es.cluster.state({ - index: newTest1Name, - metric: 'metadata', + const resolvedIndices = await es.transport.request({ + path: `_resolve/index/${nameOfIndexThatShouldBeClosed}`, }); - const test1ReindexedState = getIndexStateFromClusterState( - newTest1Name, - clusterStateResponse - ); - expect(test1ReindexedState).to.be('close'); + const test1ReindexedState = getIndexState(nameOfIndexThatShouldBeClosed, resolvedIndices); + expect(test1ReindexedState).to.be('closed'); } finally { await cleanupReindex(test1); await cleanupReindex(test2); diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts deleted file mode 100644 index d13b9836f25a10..00000000000000 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { ClusterStateAPIResponse } from '../../../plugins/upgrade_assistant/common/types'; -import { getIndexStateFromClusterState } from '../../../plugins/upgrade_assistant/common/get_index_state_from_cluster_state'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const es = getService('es'); - - describe('status and _cluster/state contract', () => { - beforeEach(async () => { - await es.indices.open({ index: '7.0-data' }); - }); - - afterEach(async () => { - await es.indices.open({ index: '7.0-data' }); - }); - - // According to https://www.elastic.co/guide/en/elasticsearch/reference/7.6/cluster-state.html - // The response from this call is considered internal and subject to change. We check that - // the contract has not changed in this integration test. - it('the _cluster/state endpoint is still what we expect', async () => { - await esArchiver.load('upgrade_assistant/reindex'); - await es.indices.close({ index: '7.0-data' }); - const result = await es.cluster.state({ - index: '7.0-data', - metric: 'metadata', - }); - - try { - if (getIndexStateFromClusterState('7.0-data', result.body) === 'close') { - return; - } - } catch (e) { - expect().fail( - `Can no longer access index open/closed state. Please update Upgrade Assistant checkup. (${e.message})` - ); - return; - } - expect().fail( - `The response contract for _cluster/state metadata has changed. Please update Upgrade Assistant checkup. Received ${JSON.stringify( - result, - null, - 2 - )}. - -Expected body.metadata.indices['7.0-data'].state to be "close".` - ); - }); - }); -} From f1ad1f1b7bd11ebf8b25d3ea95e615b92a205547 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 14 Aug 2020 10:34:08 -0500 Subject: [PATCH 24/46] [ML] Add new storeInHashQuery and replaceUrlQuery (#74955) Co-authored-by: Elastic Machine --- .../public/state_management/url/format.ts | 17 +++++++++++++++ .../url/kbn_url_storage.test.ts | 21 +++++++++++++++++++ .../state_management/url/kbn_url_storage.ts | 10 ++++++--- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/plugins/kibana_utils/public/state_management/url/format.ts b/src/plugins/kibana_utils/public/state_management/url/format.ts index 2912b665ff0147..4497e509bc86bc 100644 --- a/src/plugins/kibana_utils/public/state_management/url/format.ts +++ b/src/plugins/kibana_utils/public/state_management/url/format.ts @@ -22,6 +22,23 @@ import { stringify, ParsedQuery } from 'query-string'; import { parseUrl, parseUrlHash } from './parse'; import { url as urlUtils } from '../../../common'; +export function replaceUrlQuery( + rawUrl: string, + queryReplacer: (query: ParsedQuery) => ParsedQuery +) { + const url = parseUrl(rawUrl); + const newQuery = queryReplacer(url.query || {}); + const searchQueryString = stringify(urlUtils.encodeQuery(newQuery), { + sort: false, + encode: false, + }); + if (!url.search && !searchQueryString) return rawUrl; // nothing to change. return original url + return formatUrl({ + ...url, + search: searchQueryString, + }); +} + export function replaceUrlHashQuery( rawUrl: string, queryReplacer: (query: ParsedQuery) => ParsedQuery diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index a8c3aab2202d19..3d25134cd178d5 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -78,6 +78,27 @@ describe('kbn_url_storage', () => { const retrievedState2 = getStateFromKbnUrl('_s', newUrl); expect(retrievedState2).toEqual(state2); }); + + it('should set query to url with storeInHashQuery: false', () => { + let newUrl = setStateToKbnUrl( + '_a', + { tab: 'other' }, + { useHash: false, storeInHashQuery: false }, + 'http://localhost:5601/oxf/app/kibana/yourApp' + ); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana/yourApp?_a=(tab:other)"` + ); + newUrl = setStateToKbnUrl( + '_b', + { f: 'test', i: '', l: '' }, + { useHash: false, storeInHashQuery: false }, + newUrl + ); + expect(newUrl).toMatchInlineSnapshot( + `"http://localhost:5601/oxf/app/kibana/yourApp?_a=(tab:other)&_b=(f:test,i:'',l:'')"` + ); + }); }); describe('urlControls', () => { diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index fefd5f668c6b3a..a3b220f9115041 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -22,7 +22,7 @@ import { stringify } from 'query-string'; import { createBrowserHistory, History } from 'history'; import { decodeState, encodeState } from '../state_encoder'; import { getCurrentUrl, parseUrl, parseUrlHash } from './parse'; -import { replaceUrlHashQuery } from './format'; +import { replaceUrlHashQuery, replaceUrlQuery } from './format'; import { url as urlUtils } from '../../../common'; /** @@ -84,10 +84,14 @@ export function getStateFromKbnUrl( export function setStateToKbnUrl( key: string, state: State, - { useHash = false }: { useHash: boolean } = { useHash: false }, + { useHash = false, storeInHashQuery = true }: { useHash: boolean; storeInHashQuery?: boolean } = { + useHash: false, + storeInHashQuery: true, + }, rawUrl = window.location.href ): string { - return replaceUrlHashQuery(rawUrl, (query) => { + const replacer = storeInHashQuery ? replaceUrlHashQuery : replaceUrlQuery; + return replacer(rawUrl, (query) => { const encoded = encodeState(state, useHash); return { ...query, From bbee1f92b0879e8e4ed50a34e74edde7fe83e0c2 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 14 Aug 2020 10:31:15 -0600 Subject: [PATCH 25/46] Upgrade EUI to v27.4.0 (#74004) * eui to 27.1.0 * eui to 27.2.0 * buttoniconside type * euiselectable type * update onScroll callback and polyfill size references * findTestSubject ts * buttoncontent and collapsiblenav src snapshot updates * update prop retrieval * xpack snapshots * jest updates * type fixes * more snapshots * virtual list changes * more virtualization changes * merge * fix functional tests * data-test-subj for indexPatter-switcher * storyshots * eui to 27.3.1 * Fix unit tests * Fix broken unit test * Updated snapshots * Fixed types * search for value in euiselectable before selection * select the correct element * mock virtualized dep * ts fix * reinstate storyshot * ts fix Co-authored-by: Chandler Prall Co-authored-by: Elastic Machine --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- .../collapsible_nav.test.tsx.snap | 9557 ++++++++++------- .../header/__snapshots__/header.test.tsx.snap | 1724 ++- .../__snapshots__/modal_service.test.tsx.snap | 4 +- .../components/field/field.test.tsx | 1 - .../components/form/form.test.tsx | 1 - .../components/search/search.test.tsx | 1 - .../dashboard_empty_screen.test.tsx.snap | 65 +- .../dashboard_empty_screen.test.tsx | 1 - .../viewport/dashboard_viewport.test.tsx | 1 - .../tests/dashboard_container.test.tsx | 1 - .../shard_failure_open_modal_button.test.tsx | 1 - .../components/action_bar/action_bar.test.tsx | 1 - .../pager/tool_bar_pager_buttons.test.tsx | 1 - .../table_header/table_header.test.tsx | 1 - .../context_error_message.test.tsx | 1 - .../application/components/doc/doc.test.tsx | 1 - .../components/doc_viewer/doc_viewer.test.tsx | 1 - .../hits_counter/hits_counter.test.tsx | 1 - .../loading_spinner/loading_spinner.test.tsx | 1 - .../sidebar/discover_field.test.tsx | 1 - .../sidebar/discover_field_search.test.tsx | 1 - .../sidebar/discover_index_pattern.test.tsx | 4 +- .../sidebar/discover_sidebar.test.tsx | 1 - .../skip_bottom_button.test.tsx | 2 - .../components/table/table.test.tsx | 1 - .../timechart_header.test.tsx | 1 - .../lib/embeddables/embeddable_root.test.tsx | 1 - .../lib/panel/embeddable_panel.test.tsx | 1 - .../add_panel/add_panel_flyout.test.tsx | 3 +- .../customize_panel_action.test.ts | 2 - .../tests/customize_panel_modal.test.tsx | 1 - .../saved_objects_installer.test.js.snap | 78 +- .../header/__snapshots__/header.test.tsx.snap | 2 +- .../empty_state/empty_state.test.tsx | 1 - .../components/editor/controls_tab.test.tsx | 1 - .../editor/list_control_editor.test.tsx | 1 - .../editor/range_control_editor.test.tsx | 1 - .../components/vis/input_control_vis.test.tsx | 1 - .../inspector_panel.test.tsx.snap | 63 +- .../public/top_nav_menu/top_nav_menu_data.tsx | 4 +- .../__snapshots__/header.test.tsx.snap | 165 +- .../objects_table/components/table.test.tsx | 1 - .../components/color_picker.test.tsx | 1 - .../legend/__snapshots__/legend.test.tsx.snap | 2 +- .../__snapshots__/new_vis_modal.test.tsx.snap | 4284 +++++--- .../apps/dashboard/dashboard_filter_bar.js | 2 +- .../input_control_vis/chained_controls.js | 2 +- .../input_control_vis/dynamic_options.js | 8 +- test/functional/page_objects/discover_page.ts | 1 + .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_sample_panel_action/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- x-pack/package.json | 2 +- .../__test__/__snapshots__/List.test.tsx.snap | 18 +- .../ServiceOverview.test.tsx.snap | 5 +- .../TransactionActionMenu.test.tsx.snap | 12 +- .../time_filter.stories.storyshot | 40 +- .../asset_manager.stories.storyshot | 16 +- .../custom_element_modal.stories.storyshot | 44 +- .../element_card.stories.storyshot | 8 +- .../keyboard_shortcuts_doc.stories.storyshot | 5 + .../element_grid.stories.storyshot | 6 +- .../saved_elements_modal.stories.storyshot | 29 +- .../text_style_picker.stories.storyshot | 60 +- .../__snapshots__/toolbar.stories.storyshot | 19 +- .../delete_var.stories.storyshot | 7 +- .../__snapshots__/edit_var.stories.storyshot | 34 +- .../__snapshots__/edit_menu.stories.storyshot | 12 +- .../element_menu.stories.storyshot | 5 +- .../__snapshots__/pdf_panel.stories.storyshot | 7 +- .../share_menu.stories.storyshot | 2 +- .../__snapshots__/view_menu.stories.storyshot | 8 +- .../workpad_templates.stories.storyshot | 22 +- .../__snapshots__/shareable.test.tsx.snap | 10 +- .../__snapshots__/canvas.stories.storyshot | 6 +- .../__tests__/__snapshots__/app.test.tsx.snap | 2 +- .../__snapshots__/footer.stories.storyshot | 4 +- .../page_controls.stories.storyshot | 6 +- .../autoplay_settings.stories.storyshot | 6 +- .../__snapshots__/settings.test.tsx.snap | 2 +- .../__snapshots__/policy_table.test.js.snap | 5 +- .../layerpanel.test.tsx | 23 +- .../__snapshots__/add_license.test.js.snap | 4 +- .../request_trial_extension.test.js.snap | 8 +- .../revert_to_basic.test.js.snap | 6 +- .../__snapshots__/start_trial.test.js.snap | 8 +- .../upload_license.test.tsx.snap | 1399 ++- .../pipeline_list/pipelines_table.test.js | 2 +- .../upgrade_failure.test.js.snap | 195 +- .../entity_control/entity_control.tsx | 3 +- .../__snapshots__/no_data.test.js.snap | 8 +- .../collection_enabled.test.js.snap | 48 +- .../collection_interval.test.js.snap | 116 +- .../__snapshots__/reason_found.test.js.snap | 2 +- .../__snapshots__/summary_status.test.js.snap | 10 - .../remote_cluster_form.test.js.snap | 113 +- .../report_info_button.test.tsx.snap | 1272 ++- .../job_create/navigation/navigation.js | 2 +- .../overwritten_session_page.test.tsx.snap | 54 +- .../role_combo_box/role_combo_box.test.tsx | 37 +- .../cypress/tasks/create_new_case.ts | 2 +- .../__snapshots__/index.test.tsx.snap | 6 + .../note_card_body.test.tsx.snap | 6 + .../open_timeline_modal/index.test.tsx | 11 +- .../components/timeline/body/index.test.tsx | 2 +- .../timeline/selectable_timeline/index.tsx | 56 +- .../client_integration/policy_add.test.ts | 10 + .../public/custom_time_range_action.test.ts | 1 - .../public/custom_time_range_badge.test.ts | 1 - .../fingerprint_col.test.tsx.snap | 4 +- .../uptime_date_picker.test.tsx.snap | 12 +- .../__snapshots__/license_info.test.tsx.snap | 2 +- .../__snapshots__/ml_flyout.test.tsx.snap | 16 +- .../ml_integerations.test.tsx.snap | 7 +- .../__snapshots__/ml_job_link.test.tsx.snap | 2 +- .../__snapshots__/ml_manage_job.test.tsx.snap | 7 +- .../location_status_tags.test.tsx.snap | 6 +- .../location_missing.test.tsx.snap | 5 +- .../__snapshots__/empty_state.test.tsx.snap | 285 +- .../filter_popover.test.tsx.snap | 7 +- .../__tests__/filter_popover.test.tsx | 13 +- .../filter_status_button.test.tsx.snap | 4 +- .../__snapshots__/monitor_list.test.tsx.snap | 19 +- .../__snapshots__/status_filter.test.tsx.snap | 12 +- .../__snapshots__/page_header.test.tsx.snap | 41 +- .../threshold_watch_edit.tsx | 2 +- .../functional/apps/maps/add_layer_panel.js | 5 +- .../functional/page_objects/graph_page.ts | 3 + yarn.lock | 137 +- 131 files changed, 12036 insertions(+), 8372 deletions(-) diff --git a/package.json b/package.json index df35e5901159b2..c79c2a2f3e33a5 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.0-rc.2", "@elastic/ems-client": "7.9.3", - "@elastic/eui": "26.3.1", + "@elastic/eui": "27.4.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index ae14777e8b44a8..a37281cb2263ff 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "19.8.1", - "@elastic/eui": "26.3.1", + "@elastic/eui": "27.4.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 9ecbc055e33207..72d62730fa6980 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -378,7 +378,9 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
-
-
- -
-
-
    -
  • - -
  • -
-
+ + + +
+
-
- - -
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - > - - +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + returnFocus={[Function]} + shards={Array []} + sideCar={ + Object { + "assignMedium": [Function], + "assignSyncMedium": [Function], + "options": Object { + "async": true, + "ssr": false, + }, + "read": [Function], + "useMedium": [Function], + } + } + > + -
- + + Custom link + + + + +
-
-
-
+
- +
+ + Home + + + + +
-
-
- -
-
-
-
+ + +
+ -
-
-
-
-
-
- -
-
-
-
-
-
    -
  • - - - discover - - -
  • - visualize + recent 1
  • @@ -1442,16 +1364,18 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` class="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - dashboard + recent 2 @@ -1461,2983 +1385,3573 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
+
- -
-
-
-
+ + +
+
+
- + + visualize + + + +
  • + + + dashboard + + +
  • + +
    -
    -
    - +
    +
    +
    +
    -

    - Security -

    +
  • + + + metrics + + +
  • +
  • + + + logs + + +
  • +
    - - +
    +
    -
    -
    + +
    +
    +
    - + + siem + + + + +
    -
    -
    - -
    -
    -
    -
    + + +
    +
    +
    - + + monitoring + + + + +
    -
    -
    - + + canvas + + + + +
    -
    -
    -
      -
    • - -
    • -
    +
    + + Dock navigation + + + + +
    -
    - - -
    - } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - /> - -
    - -
    - - -
    -
    - -
    - -
    + - - - -
    -
    - - -
    - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
    -
    -
    +
    +
    +
    - -
    +
      - -

      - Recently viewed -

      -
      -
    -
    +
    + + Home + + + + +
    - - - -
    -
    - -
    +
    - -
    + + +
    +
    +
    +
    +
    -
  • - - + + recent 1 + + +
  • +
  • + - recent 2 - - -
  • - - - + + recent 2 + + + + +
    +
    +
    - - - - -
    -
    - -
    -
    - -
    - - - - - - - -

    - Kibana -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-kibana" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
    -
    - -
    -
    - -
    + + +
    -
    - + +
    - -
    - - - - - - - - - - -

    - Observability -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-observability" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
    -
    - -
    -
    - -
    + + +
    -
    - +
    - - + +
    +
    - - - -
    -
    - - - - - - - -

    - Security -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-security" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
    -
    - -
    -
    - -
    + + +
    -
    - +
    - - + +
    +
    - - - -
    -
    - - - - -

    - Management -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-management" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
    -
    - -
    -
    - -
    + + +
    -
    - +
    - - + +
    +
    - - - -
    -
    - -
    -
    - - - -
    -
    -
    - - -
    -
    - -
      - -
      + canvas + + + +
    +
    +
    +
    +
    +
      +
    • + , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" + +
    • +
    +
    +
    + + + + + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + returnFocus={[Function]} + shards={Array []} + > + + - -
    - -
    - - - - - -`; - -exports[`CollapsibleNav renders the default nav 1`] = ` - - - -`; - -exports[`CollapsibleNav renders the default nav 2`] = ` - - - - - - - -
    - -
    -
    -
    - - - -
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    + + +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + returnFocus={[Function]} + shards={Array []} + /> + + + +
    - -
    +
    + +
    +
    + + + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    + + + +
    +
    +
    +
    +
    + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    - - - -
    -
    -
    -
    - - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
    -
    -
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + - + + + + + +

    + Observability +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-observability" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" >
    - - - +
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + +
    + - + + + + + +

    + Security +

    +
    +
    +
    + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-security" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" >
    - -
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + +
    + + +

    - Recently viewed + Management

    -
    -
    - - - - - -
    - -
    -
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-management" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" >
    - -
    - + +
    + + + + +
    + +
    + +

    + Management +

    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    -

    - No recently viewed items -

    +
    + + + +
    - -
    - +
    + +
    -
    -
    -
    -
    - - - - -
    -
    - -
    - - -
    -
    + + - -
      - -
      - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" + -
    • -
    • + +
    +
    +
    +
    +
    + + +
    +
    + +
      + +
      + + Dock navigation + + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" > - Dock navigation - - - - -
    -
    -
    +
  • + +
  • + + + +
    +
    + + - - - -
    - - - - - - - -
    + + +
    + + + + close + + + + + + + + +
    + +
    + + - + > + + + + + + + + + +
    @@ -4446,7 +4960,242 @@ exports[`CollapsibleNav renders the default nav 2`] = ` `; -exports[`CollapsibleNav renders the default nav 3`] = ` +exports[`CollapsibleNav renders the default nav 1`] = ` + + + +`; + +exports[`CollapsibleNav renders the default nav 2`] = ` @@ -4682,12 +5431,16 @@ exports[`CollapsibleNav renders the default nav 3`] = ` event="keydown" handler={[Function]} /> + - -
    -
    -
    - +
    +
    +
    -
    -
    + -
    - -
    -
    -
    -
    -
    -
    -
    -

    - No recently viewed items -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    + + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" >
    -
      -
    • - -
    • -
    -
    -
    -
    - - -
    - } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - > - - -
    + + +
    + + + + close + + + + + + + + +
    + +
    - -
    + - -
    - - - - , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" + onClick={[Function]} + size="xs" + > +
  • + +
  • + + + +
    +
    + + + + + + - - close - - - - - - - - -
    - + + + +
    + + + + close + + + + + + + + +
    + +
    + + +
    diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index ce56b19f82cd09..a1920154d9f718 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -9016,953 +9016,264 @@ exports[`Header renders 3`] = ` onTouchEnd={[Function]} onTouchStart={[Function]} > - -
    -
    -
    - - -
    + lockProps={ + Object { + "allowPinchZoom": undefined, + "enabled": false, + "inert": undefined, + "shards": undefined, + "sideCar": [Function], + } } - onActivation={[Function]} - onDeactivation={[Function]} + noFocusGuards={false} persistentFocus={false} + returnFocus={true} + sideCar={[Function]} > - - -
    +
    - -
    + + +
    +
    +
    - - - -
    -
    - -
    - - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    - -
    +
    + + -
    + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" > - -
      - -
    • + +
      + + + - -
    • -
      -
    -
    -
    -
    - - - +
    + +

    + Recently viewed +

    +
    +
    +
    + + + + + +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    + + + + +
    +
    +
    -
    - -
      - -
      - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" + -
    • - +
    • + +
    +
    +
    +
    + + + +
    +
    + +
      + +
      + + Undock navigation + + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" + onClick={[Function]} + size="xs" > - Undock navigation - - - - -
    -
    -
    +
  • + +
  • + + + +
    + +
    +
    - - - -
    - - - - - - - -
    - + + + +
    + + + + close + + + + + + + + +
    + +
    + + +
    diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index b17e7d0fec7730..fb00ddc38c6dca 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -31,7 +31,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
    Modal content
    "`; +exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
    Modal content
    "`; exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ @@ -53,7 +53,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

    Some message

    "`; +exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

    Some message

    "`; exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index 72992c190e8ae2..5b33b0e0ea1204 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -25,7 +25,6 @@ import { FieldSetting } from '../../types'; import { UiSettingsType, StringValidation } from '../../../../../../core/public'; import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { Field, getEditableValue } from './field'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx index 0e942665b23a9e..e42432d0bc319f 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { UiSettingsType } from '../../../../../../core/public'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { notificationServiceMock } from '../../../../../../core/public/mocks'; diff --git a/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx index 0e3fbb3cf97fac..01f54cce603193 100644 --- a/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { Query } from '@elastic/eui'; diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 698c124d2d8057..201c6e83a4a446 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -671,35 +671,54 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` iconType="plusInCircle" size="s" > - + + +
    + + + Create new + + + + +

    diff --git a/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx b/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx index 933475d354cfab..0a49e524d33501 100644 --- a/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx +++ b/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx @@ -19,7 +19,6 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { coreMock } from '../../../../core/public/mocks'; diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx index 1e07c610b0ef27..60395bce678c2a 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx @@ -17,7 +17,6 @@ * under the License. */ -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import React from 'react'; import { skip } from 'rxjs/operators'; diff --git a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx index 6eb85faeea014d..24075e0a634baf 100644 --- a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx @@ -17,7 +17,6 @@ * under the License. */ -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import React from 'react'; import { mount } from 'enzyme'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx index 18b1237895f79d..aee8d1f4eac4d1 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx @@ -22,7 +22,6 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ShardFailureOpenModalButton } from './shard_failure_open_modal_button'; import { shardFailureRequest } from './__mocks__/shard_failure_request'; import { shardFailureResponse } from './__mocks__/shard_failure_response'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; describe('ShardFailureOpenModalButton', () => { diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx index 8976f8ea3c1075..ab7adba193d879 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ActionBar, ActionBarProps } from './action_bar'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../query_parameters/constants'; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx index 524161c77cbf8f..2cd829d89f78e4 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; test('it renders ToolBarPagerButtons', () => { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx index b201bea26503ea..224e249a274cde 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TableHeader } from './table_header'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { SortOrder } from './helpers'; import { IndexPattern, IFieldType } from '../../../../../kibana_services'; diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx index 1c9439bc34e586..1cc82479575123 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx @@ -22,7 +22,6 @@ import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; // @ts-ignore import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; describe('loading spinner', function () { diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index 0bc621714c70fb..c9fa551f61aca5 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { Doc, DocProps } from './doc'; diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx index 3710ce72533dbd..91159156903247 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx @@ -19,7 +19,6 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { DocViewer } from './doc_viewer'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { getDocViewsRegistry } from '../../../kibana_services'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx index 84ad19dbddcbfa..c2eb4f08cf5492 100644 --- a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { HitsCounter, HitsCounterProps } from './hits_counter'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; describe('hits counter', function () { diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx index 3321ac764a05ba..e996da5fe0e3c1 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { LoadingSpinner } from './loading_spinner'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; describe('loading spinner', function () { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 3f12a8c0fa7694..e1abbfd7657d06 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -18,7 +18,6 @@ */ import React from 'react'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore import StubIndexPattern from 'test_utils/stub_index_pattern'; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 654df5bfa9ee9b..625d6833406eb8 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -19,7 +19,6 @@ import React, { EventHandler, MouseEvent as ReactMouseEvent } from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { DiscoverFieldSearch, Props } from './discover_field_search'; import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui'; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx index 24e6f5993a0a5d..a1b231c2d44798 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx @@ -24,7 +24,7 @@ import { ShallowWrapper } from 'enzyme'; import { ChangeIndexPattern } from './change_indexpattern'; import { SavedObject } from 'kibana/server'; import { DiscoverIndexPattern } from './discover_index_pattern'; -import { EuiSelectable, EuiSelectableList } from '@elastic/eui'; +import { EuiSelectable } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/public'; const indexPattern = { @@ -57,7 +57,7 @@ function getIndexPatternPickerList(instance: ShallowWrapper) { } function getIndexPatternPickerOptions(instance: ShallowWrapper) { - return getIndexPatternPickerList(instance).dive().find(EuiSelectableList).prop('options'); + return getIndexPatternPickerList(instance).prop('options'); } function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 90ade60d2073de..9572f35641d69b 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -19,7 +19,6 @@ import _ from 'lodash'; import { ReactWrapper } from 'enzyme'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore import StubIndexPattern from 'test_utils/stub_index_pattern'; diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx index bf417f9f1890b6..fdb0ff973dcf03 100644 --- a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx @@ -20,8 +20,6 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { SkipBottomButton, SkipBottomButtonProps } from './skip_bottom_button'; -// @ts-ignore -import { findTestSubject } from '@elastic/eui/lib/test'; describe('Skip to Bottom Button', function () { let props: SkipBottomButtonProps; diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 29659b39693656..5b840a25d8bebd 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -18,7 +18,6 @@ */ import React from 'react'; import { mount } from 'enzyme'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewTable } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx index 964f94ca9d9b24..a4c10e749d868e 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx @@ -21,7 +21,6 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { TimechartHeader, TimechartHeaderProps } from './timechart_header'; import { EuiIconTip } from '@elastic/eui'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; describe('timechart header', function () { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx index 131069909dd2ae..cb900884fde97e 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { HelloWorldEmbeddable } from '../../../../../../examples/embeddable_examples/public'; import { EmbeddableRoot } from './embeddable_root'; import { mount } from 'enzyme'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; test('EmbeddableRoot renders an embeddable', async () => { diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 341a51d7348b2d..fcf79c1d6b211c 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 34a176400dbb9b..95aee3d9cb335f 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -30,7 +30,6 @@ import { ContainerInput } from '../../../../containers'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { coreMock } from '../../../../../../../../core/public/mocks'; -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { embeddablePluginMock } from '../../../../../mocks'; @@ -125,7 +124,7 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' notifications={core.notifications} SavedObjectFinder={(props) => } /> - ) as ReactWrapper; + ) as ReactWrapper; const spy = jest.fn(); component.instance().createNewEmbeddable = spy; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts index 6fddcbc84faf76..dbfa55a4e0f139 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -18,8 +18,6 @@ */ import { Container, isErrorEmbeddable } from '../../../..'; -// @ts-ignore -import { findTestSubject } from '@elastic/eui/lib/test'; import { nextTick } from 'test_utils/enzyme_helpers'; import { CustomizePanelTitleAction } from './customize_panel_action'; import { diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx index e094afe5284982..d12a55dd827e69 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -17,7 +17,6 @@ * under the License. */ -// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import * as React from 'react'; import { Container, isErrorEmbeddable } from '../lib'; diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap index 1e7b3d5c6284c2..9a9e055d54d2f9 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap @@ -245,22 +245,41 @@ exports[`bulkCreate should display error message when bulkCreate request fails 1 isLoading={false} onClick={[Function]} > - + + + Load Kibana objects + + +
    + +
    @@ -565,22 +584,41 @@ exports[`bulkCreate should display success message when bulkCreate is successful isLoading={false} onClick={[Function]} > - + + + Load Kibana objects + + + + + diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap index 6261ea2c907932..5218ebd1b4ad44 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/header/__snapshots__/header.test.tsx.snap @@ -33,7 +33,7 @@ exports[`Header should render normally 1`] = ` type="button" > diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index a1653c52892554..992a2fcf0c89d9 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { ButtonIconSide } from '@elastic/eui'; +import { EuiButtonProps } from '@elastic/eui'; export type TopNavMenuAction = (anchorElement: HTMLElement) => void; @@ -32,7 +32,7 @@ export interface TopNavMenuData { tooltip?: string | (() => string | undefined); emphasize?: boolean; iconType?: string; - iconSide?: ButtonIconSide; + iconSide?: EuiButtonProps['iconSide']; } export interface RegisteredTopNavMenuData extends TopNavMenuData { diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap index d56776c2be9d71..f5c2d3efd56f7e 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/header.test.tsx.snap @@ -58,45 +58,63 @@ exports[`Intro component renders correctly 1`] = ` iconType="eye" size="s" > - - -
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
    "`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
    "`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
    "`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
    "`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot index 81e75ff5ee0d9f..e0970b57ed9716 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot @@ -1398,7 +1398,7 @@ exports[`Storyshots shareables/Canvas component 1`] = ` type="button" > App renders properly 1`] = `
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot index 6ff39b723a49a9..2d3a9c460272c1 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot @@ -1351,7 +1351,7 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` type="button" > can navigate Autoplay Settings 2`] = ` type="submit" > - - -
    + +
    + + - + > + + + + + + + + + + @@ -1076,21 +1469,31 @@ exports[`UploadLicense should display a modal when license requires acknowledgem onClick={[Function]} rel="noreferrer" > - - - Cancel - + + Cancel + + - +
    @@ -1107,28 +1510,48 @@ exports[`UploadLicense should display a modal when license requires acknowledgem isLoading={false} onClick={[Function]} > - + + + Upload + + + + + +
    @@ -1734,21 +2157,31 @@ exports[`UploadLicense should display an error when ES says license is expired 1 onClick={[Function]} rel="noreferrer" > - - - Cancel - + + Cancel + + -
    + @@ -1765,28 +2198,48 @@ exports[`UploadLicense should display an error when ES says license is expired 1 isLoading={false} onClick={[Function]} > - + + + Upload + + +
    + + + @@ -2392,21 +2845,31 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 onClick={[Function]} rel="noreferrer" > - - - Cancel - + + Cancel + + -
    + @@ -2423,28 +2886,48 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 isLoading={false} onClick={[Function]} > - + + + Upload + + +
    + + + @@ -3050,21 +3533,31 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] onClick={[Function]} rel="noreferrer" > - - - Cancel - + + Cancel + + -
    + @@ -3081,28 +3574,48 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] isLoading={false} onClick={[Function]} > - + + + Upload + + +
    + + + @@ -3708,21 +4221,31 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` onClick={[Function]} rel="noreferrer" > - - - Cancel - + + Cancel + + -
    + @@ -3739,28 +4262,48 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` isLoading={false} onClick={[Function]} > - + + + Upload + + +
    + + + diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js index 2c25ae69ae722e..6c2f0d02801c4d 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js @@ -45,7 +45,7 @@ describe('PipelinesTable component', () => { it('calls clone when cloned button clicked', () => { props.pipelines = [{ id: 'testPipeline', isCentrallyManaged: true }]; const wrapper = mountWithIntl(); - wrapper.find('[iconType="copy"]').simulate('click'); + wrapper.find('[iconType="copy"]').first().simulate('click'); expect(clonePipeline).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap index 5f54513c427dd2..d0f1bed8e8e9e5 100644 --- a/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap +++ b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap @@ -265,21 +265,38 @@ exports[`UpgradeFailure component passes expected text for new pipeline 1`] = ` fill={true} onClick={[MockFunction]} > - + + + Try again + + + + + @@ -298,21 +315,31 @@ exports[`UpgradeFailure component passes expected text for new pipeline 1`] = ` onClick={[MockFunction]} type="button" > - - - Go back - + + Go back + + -
    + @@ -594,21 +621,38 @@ exports[`UpgradeFailure component passes expected text for not manual upgrade 1` fill={true} onClick={[MockFunction]} > - + + + Upgrade + + + + + @@ -627,21 +671,31 @@ exports[`UpgradeFailure component passes expected text for not manual upgrade 1` onClick={[MockFunction]} type="button" > - - - Go back - + + Go back + + -
    + @@ -923,21 +977,38 @@ exports[`UpgradeFailure component passes expected text for not new pipeline 1`] fill={true} onClick={[MockFunction]} > - + + + Try again + + + + + @@ -956,21 +1027,31 @@ exports[`UpgradeFailure component passes expected text for not new pipeline 1`] onClick={[MockFunction]} type="button" > - - - Go back - + + Go back + + -
    + diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index c144525699d81b..93bb62fa1fc58e 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -16,7 +16,6 @@ import { EuiFormRow, EuiToolTip, } from '@elastic/eui'; -import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; export interface Entity { fieldName: string; @@ -113,7 +112,7 @@ export class EntityControl extends Component { + renderOption = (option: EuiComboBoxOptionOption) => { const { label } = option; return label === EMPTY_FIELD_VALUE_LABEL ? {label} : label; }; diff --git a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap index 72f6e2b01c1297..c06f71e79e7662 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap @@ -52,7 +52,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = ` type="button" > - + + + Turn on monitoring + + + + + + diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap index 49cbe092e0e207..782be3a0730168 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap @@ -430,37 +430,59 @@ exports[`ExplainCollectionInterval collection interval setting updates should sh onClick={[Function]} type="button" > - + + + + + + Turn on monitoring + + + + + + @@ -728,28 +750,48 @@ exports[`ExplainCollectionInterval should explain about xpack.monitoring.collect onClick={[Function]} type="button" > - + + + Turn on monitoring + + + + + + diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap index 898be82b139d13..e7b88e23c5f685 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap @@ -61,7 +61,7 @@ Array [ type="button" >  Yellow

    -

    - Status [object Object] -

     Green

    -

    - Status [object Object] -

    - + +
    + + + + Save + + + + + +
    @@ -1326,21 +1348,31 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u onClick={[Function]} type="button" > - - - Show request - + + Show request + + - +
    @@ -1743,11 +1775,10 @@ Array [ type="button" > - -
    +
    + + - + > + + + + + + + + + +
    @@ -449,7 +717,9 @@ Array [
    @@ -844,27 +897,44 @@ exports[`EmptyState component doesn't render child components when count is fals color="primary" href="/app/uptime#/settings" > - - - - - Update index pattern settings - - - - + + + Update index pattern settings + + +
    + + + @@ -1256,27 +1326,45 @@ exports[`EmptyState component notifies when index does not exist 1`] = ` fill={true} href="/app/home#/tutorial/uptimeMonitors" > - - - - - View setup instructions - - - - + + + View setup instructions + + +
    + + + @@ -1288,27 +1376,44 @@ exports[`EmptyState component notifies when index does not exist 1`] = ` color="primary" href="/app/uptime#/settings" > - - - - - Update index pattern settings - - - - + + + Update index pattern settings + + +
    + + + diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap index 2677fd828c9571..63ba8bcf9d26a2 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap @@ -94,15 +94,14 @@ exports[`FilterPopover component returns selected items on popover close 1`] = ` class="euiPopover__anchor" >