diff --git a/docs/discover/images/add-field-to-pattern.png b/docs/discover/images/add-field-to-pattern.png index 9a206f5f1bd1d..54d6610ca7bb4 100644 Binary files a/docs/discover/images/add-field-to-pattern.png and b/docs/discover/images/add-field-to-pattern.png differ diff --git a/docs/discover/images/customer.png b/docs/discover/images/customer.png index 4c1ff2f2fddbd..904741631eb34 100644 Binary files a/docs/discover/images/customer.png and b/docs/discover/images/customer.png differ diff --git a/docs/discover/images/discover-from-visualize.png b/docs/discover/images/discover-from-visualize.png index 42d46e6cbd5b5..6c976f01bc9f4 100644 Binary files a/docs/discover/images/discover-from-visualize.png and b/docs/discover/images/discover-from-visualize.png differ diff --git a/docs/discover/images/discover-search-for-relevance.png b/docs/discover/images/discover-search-for-relevance.png index 64cfd87b7aac2..15945b3515530 100644 Binary files a/docs/discover/images/discover-search-for-relevance.png and b/docs/discover/images/discover-search-for-relevance.png differ diff --git a/docs/discover/images/document-table-expanded.png b/docs/discover/images/document-table-expanded.png index ebbd2e607eb5a..3abc9ee7c1cbf 100644 Binary files a/docs/discover/images/document-table-expanded.png and b/docs/discover/images/document-table-expanded.png differ diff --git a/docs/discover/images/document-table.png b/docs/discover/images/document-table.png index 5b5dbc08d6e64..98764f34350bf 100644 Binary files a/docs/discover/images/document-table.png and b/docs/discover/images/document-table.png differ diff --git a/docs/discover/images/double-arrow.png b/docs/discover/images/double-arrow.png index ba4ee11ebf738..80b87b4a35326 100644 Binary files a/docs/discover/images/double-arrow.png and b/docs/discover/images/double-arrow.png differ diff --git a/docs/discover/images/downward-arrow.png b/docs/discover/images/downward-arrow.png index 47b03cfe82b34..a0b153bfe3b39 100644 Binary files a/docs/discover/images/downward-arrow.png and b/docs/discover/images/downward-arrow.png differ diff --git a/docs/discover/images/hello-field.png b/docs/discover/images/hello-field.png index 5c6348d4e90fe..fc2c79c13a5d2 100644 Binary files a/docs/discover/images/hello-field.png and b/docs/discover/images/hello-field.png differ diff --git a/docs/discover/search-for-relevance.asciidoc b/docs/discover/search-for-relevance.asciidoc index f3cf1c3a7f52c..eab310c1b5b01 100644 --- a/docs/discover/search-for-relevance.asciidoc +++ b/docs/discover/search-for-relevance.asciidoc @@ -1,6 +1,5 @@ [[discover-search-for-relevance]] == Search for relevance -Sometimes you might be unsure which documents best match your search. {es} assigns a relevancy, or score to each document, so you can can narrow your search to the documents with the most relevant results. The higher the score, the better it matches your query. @@ -12,9 +11,7 @@ the <>, or you can use your ow . In *Discover*, open the index pattern dropdown, and select that data you want to work with. + For the sample flights data, set the index pattern to *kibana_sample_data_flights*. -. In the query bar, click *KQL*, and then turn it off. -+ -You're now using the <>. + . Run your search. For the sample data, try: + ```ts @@ -22,15 +19,15 @@ Warsaw OR Venice OR Clear ``` . If you don't see any results, expand the <>, for example to *Last 7 days*. . From the list of *Available fields*, add `_score` and any other fields you want to the document table. -. To sort the `_score` column in descending order, hover over its header, and then click twice on -the arrow icon -image:images/double-arrow.png[Double arrow icon to indicate sorting] so it changes to +. To sort the `_score` column in descending order, hover over its header, and set +the sort icon to image:images/downward-arrow.png[Downward pointing arrow to indicate descending sorting]. + At this point, you're doing a multi-column sort: first by `Time`, and then by `_score`. -. To turn off sorting for the `Time` field, hover over its header, and then click the down arrow. +. To turn off sorting for the `Time` field, hover over its header, and set the sort icon to +image:images/double-arrow.png[Arrow on both ends of the icon indicates sorting is off]. + Your table now sorts documents from most to least relevant. + [role="screenshot"] -image::images/discover-search-for-relevance.png["Example of a search for relevance"] +image::images/discover-search-for-relevance.png["Documents are sorted from most relevant to least relevant."] diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 1e716a840095d..e52531f9decdc 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -78,7 +78,7 @@ If you are using the sample data, this value was set when you added the data. If you are using your own data, and it does not have a time field, the range selection is not available. . To view the count of documents for a given time in the specified range, -click and drag the mouse over the histogram. +click and drag the mouse over the chart. [float] [[explore-fields-in-your-data]] @@ -108,7 +108,7 @@ them to your document table. Your table should look similar to this: image:images/document-table.png[Document table with fields for manufacturer, customer_first_name, and customer_last_name] . To rearrange the table columns, hover the mouse over a -column header, and then use the move controls. +column header, and then use the move control. . To view more of the document table, click *Hide chart*. @@ -275,7 +275,7 @@ image:images/discover-maps.png[Map containing documents] [[share-your-findings]] === Share your findings -To share your findings with a larger audience, click *Share* in the toolbar. For detailed information about the sharing options, refer to <>. +To share your findings with a larger audience, click *Share* in the *Discover* toolbar. For detailed information about the sharing options, refer to <>. [float] @@ -285,8 +285,6 @@ To share your findings with a larger audience, click *Share* in the toolbar. For * <>. -* <>. - * <> to better meet your needs. Go to **Advanced Settings** to configure the number of documents to show, the table columns that display by default, and more. diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 0972f5cd2d145..0d7365c4b97c1 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -566,6 +566,25 @@ describe('SavedObjectsRepository', () => { await test(namespace); }); + it(`normalizes initialNamespaces from 'default' to undefined`, async () => { + const test = async (namespace) => { + const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }]; + await bulkCreateSuccess(objects, { namespace, overwrite: true }); + const body = [ + { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) }, + expect.not.objectContaining({ namespace: 'default' }), + ]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + client.mget.mockClear(); + }; + await test(undefined); + await test(namespace); + }); + it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => { const test = async (namespace) => { const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; @@ -2131,6 +2150,24 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes initialNamespaces from 'default' to undefined`, async () => { + await savedObjectsRepository.create('dashboard', attributes, { + id, + namespace, + initialNamespaces: ['default'], + }); + + expect(client.create).toHaveBeenCalledTimes(1); + expect(client.create).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + id: `dashboard:${id}`, + body: expect.not.objectContaining({ namespace: 'default' }), + }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id or add namespace or namespaces fields when using namespace-agnostic type`, async () => { await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); expect(client.create).toHaveBeenCalledWith( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 09c0de4daca90..e49b2e413981f 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -301,7 +301,9 @@ export class SavedObjectsRepository { let savedObjectNamespaces: string[] | undefined; if (this._registry.isSingleNamespace(type)) { - savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace; + savedObjectNamespace = initialNamespaces + ? normalizeNamespace(initialNamespaces[0]) + : namespace; } else if (this._registry.isMultiNamespace(type)) { if (id && overwrite) { // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces @@ -486,7 +488,9 @@ export class SavedObjectsRepository { versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(object.type)) { - savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace; + savedObjectNamespace = initialNamespaces + ? normalizeNamespace(initialNamespaces[0]) + : namespace; } else if (this._registry.isMultiNamespace(object.type)) { savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); } diff --git a/src/plugins/discover/public/application/angular/_index.scss b/src/plugins/discover/public/application/angular/_index.scss deleted file mode 100644 index b0e5b6e3edf7b..0000000000000 --- a/src/plugins/discover/public/application/angular/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'directives/index'; -@import 'context/index'; diff --git a/src/plugins/discover/public/application/angular/context/NOTES.md b/src/plugins/discover/public/application/angular/context/NOTES.md deleted file mode 100644 index 7aaa251348961..0000000000000 --- a/src/plugins/discover/public/application/angular/context/NOTES.md +++ /dev/null @@ -1,95 +0,0 @@ -# Discover Context App Implementation Notes - -The implementation of this app is intended to exhibit certain desirable -properties by adhering to a set of *principles*. This document aims to explain -those and the *concepts* employed to achieve that. - - -## Principles - -**Single Source of Truth**: A good user experience depends on the UI displaying -consistent information across the whole page. To achieve this, there should -always be a single source of truth for the application's state. In this -application this is the `ContextAppController::state` object. - -**Unidirectional Data Flow**: While a single state promotes rendering -consistency, it does little to make the state changes easier to reason about. -To avoid having state mutations scattered all over the code, this app -implements a unidirectional data flow architecture. That means that the state -is treated as immutable throughout the application except for actions, which -may modify it to cause angular to re-render and watches to trigger. - -**Unit-Testability**: Creating unit tests for large parts of the UI code is -made easy by expressing the as much of the logic as possible as -side-effect-free functions. The only place where side-effects are allowed are -actions. Due to the nature of AngularJS a certain amount of impure code must be -employed in some cases, e.g. when dealing with the isolate scope bindings in -`ContextAppController`. - -**Loose Coupling**: An attempt was made to couple the parts that make up this -app as loosely as possible. This means using pure functions whenever possible -and isolating the angular directives diligently. To that end, the app has been -implemented as the independent `ContextApp` directive in [app.js](app.js). It -does not access the Kibana `AppState` directly but communicates only via its -directive properties. The binding of these attributes to the state and thereby -to the route is performed by the `CreateAppRouteController`in -[index.js](index.js). Similarly, the `SizePicker` directive only communicates -with its parent via the passed properties. - - -## Concepts - -To adhere to the principles mentioned above, this app borrows some concepts -from the redux architecture that forms a ciruclar unidirectional data flow: - -``` - - |* create initial state - v - +->+ - | v - | |* state - | v - | |* angular templates render state - | v - | |* angular calls actions in response to user action/system events - | v - | |* actions modify state - | v - +--+ - -``` - -**State**: The state is the single source of truth at -`ContextAppController::state` and may only be modified by actions. - -**Action**: Actions are functions that are called in response to user or system -actions and may modified the state the are bound to via their closure. - - -## Directory Structure - -**index.js**: Defines the route and renders the `` directive, -binding it to the `AppState`. - -**app.js**: Defines the `` directive, that is at the root of the -application. Creates the store, reducer and bound actions/selectors. - -**query**: Exports the actions, reducers and selectors related to the -query status and results. - -**query_parameters**: Exports the actions, reducers and selectors related to -the parameters used to construct the query. - -**components/action_bar**: Defines the `` -directive including its respective styles. - - -**api/anchor.js**: Exports `fetchAnchor()` that creates and executes the -query for the anchor document. - -**api/context.js**: Exports `fetchPredecessors()`, `fetchSuccessors()`, `fetchSurroundingDocs()` that -create and execute the queries for the preceeding and succeeding documents. - -**api/utils**: Exports various functions used to create and transform -queries. diff --git a/src/plugins/discover/public/application/angular/context/_index.scss b/src/plugins/discover/public/application/angular/context/_index.scss deleted file mode 100644 index 49293fa1b8ad8..0000000000000 --- a/src/plugins/discover/public/application/angular/context/_index.scss +++ /dev/null @@ -1,8 +0,0 @@ -// Prefix all styles with "cxt" to avoid conflicts. -// Examples -// cxtChart -// cxtChart__legend -// cxtChart__legend--small -// cxtChart__legend-isLoading - -@import 'components/action_bar/index'; diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/_index.scss b/src/plugins/discover/public/application/angular/context/components/action_bar/_index.scss deleted file mode 100644 index d54e2caffc122..0000000000000 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'action_bar'; diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts deleted file mode 100644 index dd8c874391fd4..0000000000000 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts +++ /dev/null @@ -1,15 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getAngularModule } from '../../../../../kibana_services'; -import { ActionBar } from './action_bar'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -getAngularModule().directive('contextActionBar', function (reactDirective: any) { - return reactDirective(ActionBar); -}); diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/index.ts b/src/plugins/discover/public/application/angular/context/components/action_bar/index.ts deleted file mode 100644 index 6e09466f6a3ec..0000000000000 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/index.ts +++ /dev/null @@ -1,9 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import './action_bar_directive'; diff --git a/src/plugins/discover/public/application/angular/context/helpers/call_after_bindings_workaround.js b/src/plugins/discover/public/application/angular/context/helpers/call_after_bindings_workaround.js deleted file mode 100644 index 6ba0e3db40eeb..0000000000000 --- a/src/plugins/discover/public/application/angular/context/helpers/call_after_bindings_workaround.js +++ /dev/null @@ -1,52 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * WHAT NEEDS THIS WORKAROUND? - * =========================== - * Any directive that meets all of the following criteria: - * - uses isolate scope bindings - * - sets `bindToController: true` - * - synchronously accesses the bound values in the controller constructor - * - * - * - * HOW DO I GET RID OF IT? - * ======================= - * The quick band-aid solution: - * Wrap your constructor logic so it doesn't access bound values - * synchronously. This can have subtle bugs which is why I didn't - * just wrap all of the offenders in $timeout() and made this - * workaround instead. - * - * The more complete solution: - * Use the new component lifecycle methods, like `$onInit()`, to access - * bindings immediately after the constructor is called, which shouldn't - * have any observable effect outside of the constructor. - * - * NOTE: `$onInit()` is not dependency injected, if you need controller specific - * dependencies like `$scope` then you're probably using watchers and should - * take a look at the new one-way data flow facilities available to - * directives/components: - * - * https://docs.angularjs.org/guide/component#component-based-application-architecture - * - */ - -export function callAfterBindingsWorkaround(constructor) { - return function InitAfterBindingsWrapper($injector, $attrs, $element, $scope, $transclude) { - this.$onInit = () => { - $injector.invoke(constructor, this, { - $attrs, - $element, - $scope, - $transclude, - }); - }; - }; -} diff --git a/src/plugins/discover/public/application/angular/directives/_histogram.scss b/src/plugins/discover/public/application/angular/directives/_histogram.scss deleted file mode 100644 index 948f438eea542..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/_histogram.scss +++ /dev/null @@ -1,4 +0,0 @@ -.dscHistogram__header--partial { - font-weight: $euiFontWeightRegular; - min-width: $euiSize * 12; -} diff --git a/src/plugins/discover/public/application/angular/directives/_index.scss b/src/plugins/discover/public/application/angular/directives/_index.scss deleted file mode 100644 index dfacdf45c9d7b..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'histogram'; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js deleted file mode 100644 index e2a0a19b80cf0..0000000000000 --- a/src/plugins/discover/public/application/angular/discover.js +++ /dev/null @@ -1,39 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getAngularModule, getServices } from '../../kibana_services'; - -const services = getServices(); - -const { history: getHistory } = getServices(); - -const app = getAngularModule(); - -app.directive('discoverApp', function () { - return { - restrict: 'E', - controllerAs: 'discoverApp', - controller: discoverController, - }; -}); - -function discoverController(_, $scope) { - const history = getHistory(); - - $scope.opts = { - history, - services, - navigateTo: (path) => { - $scope.$evalAsync(() => { - history.push(path); - }); - }, - }; - - $scope.$on('$destroy', () => {}); -} diff --git a/src/plugins/discover/public/application/angular/discover_datagrid.html b/src/plugins/discover/public/application/angular/discover_datagrid.html deleted file mode 100644 index fa1181fd0d6cd..0000000000000 --- a/src/plugins/discover/public/application/angular/discover_datagrid.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html deleted file mode 100644 index 025a4490faf1f..0000000000000 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/plugins/discover/public/application/angular/doc_viewer.tsx b/src/plugins/discover/public/application/angular/doc_viewer.tsx deleted file mode 100644 index 2b51b68b2fb34..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_viewer.tsx +++ /dev/null @@ -1,38 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { DocViewer } from '../components/doc_viewer/doc_viewer'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDocViewerDirective(reactDirective: any) { - return reactDirective( - (props: React.ComponentProps) => { - return ; - }, - [ - 'hit', - ['indexPattern', { watchDepth: 'reference' }], - ['filter', { watchDepth: 'reference' }], - ['columns', { watchDepth: 'collection' }], - ['onAddColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ], - { - restrict: 'E', - scope: { - hit: '=', - indexPattern: '=', - filter: '=?', - columns: '=?', - onAddColumn: '=?', - onRemoveColumn: '=?', - }, - } - ); -} diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts deleted file mode 100644 index 5d2da54980801..0000000000000 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ /dev/null @@ -1,142 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// inner angular imports -// these are necessary to bootstrap the local angular. -// They can stay even after NP cutover -import '../index.scss'; -import angular from 'angular'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; -import { EuiIcon } from '@elastic/eui'; -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { CoreStart, PluginInitializerContext } from 'kibana/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { Storage } from '../../../../kibana_utils/public'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public'; -import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive'; -import { createDiscoverGridDirective } from './create_discover_grid_directive'; -import { - configureAppAngularModule, - PrivateProvider, - registerListenEventListener, - watchMultiDecorator, -} from '../../../../kibana_legacy/public'; -import { PromiseServiceCreator } from './helpers'; -import { DiscoverStartPlugins } from '../../plugin'; -import { getScopedHistory } from '../../kibana_services'; - -/** - * returns the main inner angular module, it contains all the parts of Angular Discover - * needs to render, so in the end the current 'kibana' angular module is no longer necessary - */ -export function getInnerAngularModule( - name: string, - core: CoreStart, - deps: DiscoverStartPlugins, - context: PluginInitializerContext -) { - const module = initializeInnerAngularModule(name, core, deps.navigation, deps.data); - configureAppAngularModule(module, { core, env: context.env }, true, getScopedHistory); - return module; -} - -/** - * returns a slimmer inner angular module for embeddable rendering - */ -export function getInnerAngularModuleEmbeddable( - name: string, - core: CoreStart, - deps: DiscoverStartPlugins -) { - return initializeInnerAngularModule(name, core, deps.navigation, deps.data, true); -} - -let initialized = false; - -export function initializeInnerAngularModule( - name = 'app/discover', - core: CoreStart, - navigation: NavigationStart, - data: DataPublicPluginStart, - embeddable = false -) { - if (!initialized) { - createLocalI18nModule(); - createLocalPrivateModule(); - createLocalPromiseModule(); - createLocalStorageModule(); - createDocTableModule(); - initialized = true; - } - - if (embeddable) { - return angular - .module(name, [ - 'ngSanitize', - 'react', - 'ui.bootstrap', - 'discoverI18n', - 'discoverPrivate', - 'discoverDocTable', - 'discoverPromise', - ]) - .config(watchMultiDecorator) - .directive('icon', (reactDirective) => reactDirective(EuiIcon)); - } - - return angular - .module(name, [ - 'ngSanitize', - 'react', - 'ui.bootstrap', - 'discoverI18n', - 'discoverPrivate', - 'discoverPromise', - 'discoverLocalStorageProvider', - 'discoverDocTable', - ]) - .config(watchMultiDecorator) - .run(registerListenEventListener); -} - -function createLocalPromiseModule() { - angular.module('discoverPromise', []).service('Promise', PromiseServiceCreator); -} - -function createLocalPrivateModule() { - angular.module('discoverPrivate', []).provider('Private', PrivateProvider); -} - -function createLocalI18nModule() { - angular - .module('discoverI18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} - -function createLocalStorageModule() { - angular - .module('discoverLocalStorageProvider', ['discoverPrivate']) - .service('localStorage', createLocalStorageService('localStorage')) - .service('sessionStorage', createLocalStorageService('sessionStorage')); -} - -const createLocalStorageService = function (type: string) { - return function ($window: ng.IWindowService) { - return new Storage($window[type]); - }; -}; - -function createDocTableModule() { - angular - .module('discoverDocTable', ['react']) - .directive('discoverGrid', createDiscoverGridDirective) - .directive('contextAppLegacy', createContextAppLegacy); -} diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts deleted file mode 100644 index a7d9d4581d989..0000000000000 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ /dev/null @@ -1,10 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { handleSourceColumnState } from './state_helpers'; -export { PromiseServiceCreator } from './promises'; diff --git a/src/plugins/discover/public/application/angular/helpers/promises.d.ts b/src/plugins/discover/public/application/angular/helpers/promises.d.ts deleted file mode 100644 index a841d43ae3400..0000000000000 --- a/src/plugins/discover/public/application/angular/helpers/promises.d.ts +++ /dev/null @@ -1,9 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function PromiseServiceCreator($q: unknown, $timeout: unknown): (fn: unknown) => unknown; diff --git a/src/plugins/discover/public/application/angular/helpers/promises.js b/src/plugins/discover/public/application/angular/helpers/promises.js deleted file mode 100644 index 7da98f8274964..0000000000000 --- a/src/plugins/discover/public/application/angular/helpers/promises.js +++ /dev/null @@ -1,118 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; - -export function PromiseServiceCreator($q, $timeout) { - function Promise(fn) { - if (typeof this === 'undefined') - throw new Error('Promise constructor must be called with "new"'); - - const defer = $q.defer(); - try { - fn(defer.resolve, defer.reject); - } catch (e) { - defer.reject(e); - } - return defer.promise; - } - - Promise.all = Promise.props = $q.all; - Promise.resolve = function (val) { - const defer = $q.defer(); - defer.resolve(val); - return defer.promise; - }; - Promise.reject = function (reason) { - const defer = $q.defer(); - defer.reject(reason); - return defer.promise; - }; - Promise.cast = $q.when; - Promise.delay = function (ms) { - return $timeout(_.noop, ms); - }; - Promise.method = function (fn) { - return function () { - const args = Array.prototype.slice.call(arguments); - return Promise.try(fn, args, this); - }; - }; - Promise.nodeify = function (promise, cb) { - promise.then(function (val) { - cb(void 0, val); - }, cb); - }; - Promise.map = function (arr, fn) { - return Promise.all( - arr.map(function (i, el, list) { - return Promise.try(fn, [i, el, list]); - }) - ); - }; - Promise.each = function (arr, fn) { - const queue = arr.slice(0); - let i = 0; - return (function next() { - if (!queue.length) return arr; - return Promise.try(fn, [arr.shift(), i++]).then(next); - })(); - }; - Promise.is = function (obj) { - // $q doesn't create instances of any constructor, promises are just objects with a then function - // https://github.com/angular/angular.js/blob/58f5da86645990ef984353418cd1ed83213b111e/src/ng/q.js#L335 - return obj && typeof obj.then === 'function'; - }; - Promise.halt = _.once(function () { - const promise = new Promise(() => {}); - promise.then = _.constant(promise); - promise.catch = _.constant(promise); - return promise; - }); - Promise.try = function (fn, args, ctx) { - if (typeof fn !== 'function') { - return Promise.reject(new TypeError('fn must be a function')); - } - - let value; - - if (Array.isArray(args)) { - try { - value = fn.apply(ctx, args); - } catch (e) { - return Promise.reject(e); - } - } else { - try { - value = fn.call(ctx, args); - } catch (e) { - return Promise.reject(e); - } - } - - return Promise.resolve(value); - }; - Promise.fromNode = function (takesCbFn) { - return new Promise(function (resolve, reject) { - takesCbFn(function (err, ...results) { - if (err) reject(err); - else if (results.length > 1) resolve(results); - else resolve(results[0]); - }); - }); - }; - Promise.race = function (iterable) { - return new Promise((resolve, reject) => { - for (const i of iterable) { - Promise.resolve(i).then(resolve, reject); - } - }); - }; - - return Promise; -} diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover/public/application/angular/index.ts deleted file mode 100644 index 643823a15ffcd..0000000000000 --- a/src/plugins/discover/public/application/angular/index.ts +++ /dev/null @@ -1,13 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// required for i18nIdDirective -import 'angular-sanitize'; - -import './doc'; -import './context'; diff --git a/src/plugins/discover/public/application/application.ts b/src/plugins/discover/public/application/application.ts index cca5c1f112bb8..c0294ca043895 100644 --- a/src/plugins/discover/public/application/application.ts +++ b/src/plugins/discover/public/application/application.ts @@ -6,11 +6,10 @@ * Side Public License, v 1. */ -import './index.scss'; import { renderApp as renderReactApp } from './index'; /** - * Here's where Discover's inner angular is mounted and rendered + * Here's where Discover is mounted and rendered */ export async function renderApp(moduleName: string, element: HTMLElement) { const app = mountDiscoverApp(moduleName, element); diff --git a/src/plugins/discover/public/application/components/context_app/__mocks__/top_nav_menu.tsx b/src/plugins/discover/public/application/apps/context/__mocks__/top_nav_menu.tsx similarity index 100% rename from src/plugins/discover/public/application/components/context_app/__mocks__/top_nav_menu.tsx rename to src/plugins/discover/public/application/apps/context/__mocks__/top_nav_menu.tsx diff --git a/src/plugins/discover/public/application/components/context_app/__mocks__/use_context_app_fetch.tsx b/src/plugins/discover/public/application/apps/context/__mocks__/use_context_app_fetch.tsx similarity index 100% rename from src/plugins/discover/public/application/components/context_app/__mocks__/use_context_app_fetch.tsx rename to src/plugins/discover/public/application/apps/context/__mocks__/use_context_app_fetch.tsx diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/_action_bar.scss b/src/plugins/discover/public/application/apps/context/components/action_bar/_action_bar.scss similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/_action_bar.scss rename to src/plugins/discover/public/application/apps/context/components/action_bar/_action_bar.scss diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.test.tsx similarity index 95% rename from src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx rename to src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.test.tsx index 357eecb29f4a2..de6d01b6a5273 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx +++ b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.test.tsx @@ -10,11 +10,8 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ActionBar, ActionBarProps } from './action_bar'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { - MAX_CONTEXT_SIZE, - MIN_CONTEXT_SIZE, -} from '../../../../components/context_app/utils/constants'; -import { SurrDocType } from '../../api/context'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../utils/constants'; +import { SurrDocType } from '../../services/context'; describe('Test Discover Context ActionBar for successor | predecessor records', () => { [SurrDocType.SUCCESSORS, SurrDocType.PREDECESSORS].forEach((type) => { diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx similarity index 97% rename from src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx rename to src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx index 843e24b189824..634e6d2c90a91 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import './_action_bar.scss'; import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; @@ -18,11 +19,8 @@ import { EuiSpacer, } from '@elastic/eui'; import { ActionBarWarning } from './action_bar_warning'; -import { SurrDocType } from '../../api/context'; -import { - MAX_CONTEXT_SIZE, - MIN_CONTEXT_SIZE, -} from '../../../../components/context_app/utils/constants'; +import { SurrDocType } from '../../services/context'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../utils/constants'; export interface ActionBarProps { /** diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar_warning.tsx similarity index 97% rename from src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx rename to src/plugins/discover/public/application/apps/context/components/action_bar/action_bar_warning.tsx index 2d189ad4c9b9c..342485fe1f8c0 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx +++ b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar_warning.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut } from '@elastic/eui'; -import { SurrDocType } from '../../api/context'; +import { SurrDocType } from '../../services/context'; export function ActionBarWarning({ docCount, type }: { docCount: number; type: SurrDocType }) { if (type === SurrDocType.PREDECESSORS) { diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.test.tsx similarity index 95% rename from src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx rename to src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.test.tsx index 7f7d92be03103..47937058451fa 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx +++ b/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; -import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; +import { FailureReason, LoadingStatus } from '../../services/context_query_state'; import { findTestSubject } from '@elastic/eui/lib/test'; describe('loading spinner', function () { diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx similarity index 97% rename from src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx rename to src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx index 7ac1ef60ed3c2..fac948d0f7040 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx +++ b/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx @@ -13,7 +13,7 @@ import { FailureReason, LoadingStatus, LoadingStatusEntry, -} from '../../angular/context_query_state'; +} from '../../services/context_query_state'; export interface ContextErrorMessageProps { /** diff --git a/src/plugins/discover/public/application/components/context_error_message/index.ts b/src/plugins/discover/public/application/apps/context/components/context_error_message/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/context_error_message/index.ts rename to src/plugins/discover/public/application/apps/context/components/context_error_message/index.ts diff --git a/src/plugins/discover/public/application/components/context_app/context_app.scss b/src/plugins/discover/public/application/apps/context/context_app.scss similarity index 88% rename from src/plugins/discover/public/application/components/context_app/context_app.scss rename to src/plugins/discover/public/application/apps/context/context_app.scss index 9ff36ca452742..fa29b0513055c 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.scss +++ b/src/plugins/discover/public/application/apps/context/context_app.scss @@ -1,4 +1,4 @@ -@import '../../../../../../core/public/mixins'; +@import 'src/core/public/mixins'; .dscDocsPage { @include kibanaFullBodyHeight(54px); // action bar height diff --git a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx b/src/plugins/discover/public/application/apps/context/context_app.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/context_app/context_app.test.tsx rename to src/plugins/discover/public/application/apps/context/context_app.test.tsx diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/apps/context/context_app.tsx similarity index 94% rename from src/plugins/discover/public/application/components/context_app/context_app.tsx rename to src/plugins/discover/public/application/apps/context/context_app.tsx index 4121beab1dd2e..6198ced1550bb 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app.tsx @@ -14,17 +14,17 @@ import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { esFilters, SortDirection } from '../../../../../data/public'; import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; -import { ContextErrorMessage } from '../context_error_message'; +import { ContextErrorMessage } from './components/context_error_message'; import { IndexPattern, IndexPatternField } from '../../../../../data/common'; -import { LoadingStatus } from '../../angular/context_query_state'; +import { LoadingStatus } from './services/context_query_state'; import { getServices } from '../../../kibana_services'; -import { AppState, isEqualFilters } from '../../angular/context_state'; +import { AppState, isEqualFilters } from './services/context_state'; import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; -import { useContextAppState } from './use_context_app_state'; -import { useContextAppFetch } from './use_context_app_fetch'; +import { useContextAppState } from './utils/use_context_app_state'; +import { useContextAppFetch } from './utils/use_context_app_fetch'; import { popularizeField } from '../../helpers/popularize_field'; import { ContextAppContent } from './context_app_content'; -import { SurrDocType } from '../../angular/context/api/context'; +import { SurrDocType } from './services/context'; import { DocViewFilterFn } from '../../doc_views/doc_views_types'; const ContextAppContentMemoized = memo(ContextAppContent); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx b/src/plugins/discover/public/application/apps/context/context_app_content.test.tsx similarity index 88% rename from src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx rename to src/plugins/discover/public/application/apps/context/context_app_content.test.tsx index 1b95af8bdbe1c..17caef09c4c07 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app_content.test.tsx @@ -9,17 +9,17 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; -import { AppState, GetStateReturn } from '../../angular/context_state'; +import { ActionBar } from './components/action_bar/action_bar'; +import { AppState, GetStateReturn } from './services/context_state'; import { SortDirection } from 'src/plugins/data/common'; -import { EsHitRecordList } from '../../angular/context/api/context'; import { ContextAppContent, ContextAppContentProps } from './context_app_content'; import { getServices, setServices } from '../../../kibana_services'; -import { LoadingStatus } from '../../angular/context_query_state'; +import { LoadingStatus } from './services/context_query_state'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { DiscoverGrid } from '../discover_grid/discover_grid'; +import { DiscoverGrid } from '../../components/discover_grid/discover_grid'; import { discoverServiceMock } from '../../../__mocks__/services'; -import { DocTableWrapper } from '../../apps/main/components/doc_table/doc_table_wrapper'; +import { DocTableWrapper } from '../main/components/doc_table/doc_table_wrapper'; +import { EsHitRecordList } from '../../types'; describe('ContextAppContent test', () => { let hit; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/apps/context/context_app_content.tsx similarity index 92% rename from src/plugins/discover/public/application/components/context_app/context_app_content.tsx rename to src/plugins/discover/public/application/apps/context/context_app_content.tsx index 78c354cbf908d..19b6bfac2876c 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app_content.tsx @@ -12,15 +12,16 @@ import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; import { IndexPattern } from '../../../../../data/common'; import { SortDirection } from '../../../../../data/public'; -import { LoadingStatus } from '../../angular/context_query_state'; -import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; -import { DiscoverGrid } from '../discover_grid/discover_grid'; +import { LoadingStatus } from './services/context_query_state'; +import { ActionBar } from './components/action_bar/action_bar'; +import { DiscoverGrid } from '../../components/discover_grid/discover_grid'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { AppState } from '../../angular/context_state'; -import { EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; +import { AppState } from './services/context_state'; +import { SurrDocType } from './services/context'; import { DiscoverServices } from '../../../build_services'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; -import { DocTableContext } from '../../apps/main/components/doc_table/doc_table_context'; +import { DocTableContext } from '../main/components/doc_table/doc_table_context'; +import { EsHitRecordList } from '../../types'; export interface ContextAppContentProps { columns: string[]; diff --git a/src/plugins/discover/public/application/apps/context/context_app_route.tsx b/src/plugins/discover/public/application/apps/context/context_app_route.tsx index 7ced3955c70e7..4bade3d03d993 100644 --- a/src/plugins/discover/public/application/apps/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app_route.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { DiscoverServices } from '../../../build_services'; -import { ContextApp } from '../../components/context_app/context_app'; +import { ContextApp } from './context_app'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; import { useIndexPattern } from '../../helpers/use_index_pattern'; diff --git a/src/plugins/discover/public/application/angular/context/api/__snapshots__/context.test.ts.snap b/src/plugins/discover/public/application/apps/context/services/__snapshots__/context.test.ts.snap similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/__snapshots__/context.test.ts.snap rename to src/plugins/discover/public/application/apps/context/services/__snapshots__/context.test.ts.snap diff --git a/src/plugins/discover/public/application/angular/context/api/_stubs.ts b/src/plugins/discover/public/application/apps/context/services/_stubs.ts similarity index 98% rename from src/plugins/discover/public/application/angular/context/api/_stubs.ts rename to src/plugins/discover/public/application/apps/context/services/_stubs.ts index 241d0a621f245..d0318c9efbab7 100644 --- a/src/plugins/discover/public/application/angular/context/api/_stubs.ts +++ b/src/plugins/discover/public/application/apps/context/services/_stubs.ts @@ -10,7 +10,7 @@ import sinon from 'sinon'; import moment from 'moment'; import { IndexPatternsContract } from '../../../../../../data/public'; -import { EsHitRecordList } from './context'; +import { EsHitRecordList } from '../../../types'; type SortHit = { [key in string]: number; // timeField name diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/apps/context/services/anchor.test.ts similarity index 99% rename from src/plugins/discover/public/application/angular/context/api/anchor.test.ts rename to src/plugins/discover/public/application/apps/context/services/anchor.test.ts index 932c7398fc951..b58369781a4af 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/anchor.test.ts @@ -9,9 +9,9 @@ import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; import { fetchAnchorProvider, updateSearchSource } from './anchor'; -import { EsHitRecord, EsHitRecordList } from './context'; import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { EsHitRecord, EsHitRecordList } from '../../../types'; describe('context app', function () { let fetchAnchor: ( diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.ts b/src/plugins/discover/public/application/apps/context/services/anchor.ts similarity index 97% rename from src/plugins/discover/public/application/angular/context/api/anchor.ts rename to src/plugins/discover/public/application/apps/context/services/anchor.ts index 06ca4bd4afa62..2d64f7526ffdd 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.ts +++ b/src/plugins/discover/public/application/apps/context/services/anchor.ts @@ -15,7 +15,7 @@ import { EsQuerySortValue, IndexPattern, } from '../../../../../../data/public'; -import { EsHitRecord } from './context'; +import { EsHitRecord } from '../../../types'; export function fetchAnchorProvider( indexPatterns: IndexPatternsContract, diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts b/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts similarity index 98% rename from src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts rename to src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts index 127616e27fd92..a503715f4b5e2 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts @@ -9,11 +9,11 @@ import moment from 'moment'; import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; -import { EsHitRecordList, fetchContextProvider, SurrDocType } from './context'; +import { fetchContextProvider, SurrDocType } from './context'; import { setServices, SortDirection } from '../../../../kibana_services'; -import { EsHitRecord } from './context'; import { Query } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; +import { EsHitRecord, EsHitRecordList } from '../../../types'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts b/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts similarity index 98% rename from src/plugins/discover/public/application/angular/context/api/context.successors.test.ts rename to src/plugins/discover/public/application/apps/context/services/context.successors.test.ts index a6c4a734fdbc4..fcd1bad487c4e 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts @@ -12,9 +12,9 @@ import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { setServices, SortDirection } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; -import { EsHitRecordList, fetchContextProvider, SurrDocType } from './context'; -import { EsHitRecord } from './context'; +import { fetchContextProvider, SurrDocType } from './context'; import { DiscoverServices } from '../../../../build_services'; +import { EsHitRecord, EsHitRecordList } from '../../../types'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); diff --git a/src/plugins/discover/public/application/angular/context/api/context.test.ts b/src/plugins/discover/public/application/apps/context/services/context.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/context.test.ts rename to src/plugins/discover/public/application/apps/context/services/context.test.ts diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/apps/context/services/context.ts similarity index 90% rename from src/plugins/discover/public/application/angular/context/api/context.ts rename to src/plugins/discover/public/application/apps/context/services/context.ts index b6ba95fd5e84a..237de8e52e656 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.ts @@ -5,32 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import type { estypes } from '@elastic/elasticsearch'; -import { Filter, IndexPatternsContract, IndexPattern, SearchSource } from 'src/plugins/data/public'; +import { Filter, IndexPattern, IndexPatternsContract, SearchSource } from 'src/plugins/data/public'; import { reverseSortDir, SortDirection } from './utils/sorting'; -import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; +import { convertIsoToMillis, extractNanos } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; import { generateIntervals } from './utils/generate_intervals'; import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; +import { EsHitRecord, EsHitRecordList } from '../../../types'; export enum SurrDocType { SUCCESSORS = 'successors', PREDECESSORS = 'predecessors', } -export type EsHitRecord = Required< - Pick -> & { - _source?: Record; - _score?: number; - isAnchor?: boolean; -}; - -export type EsHitRecordList = EsHitRecord[]; - const DAY_MILLIS = 24 * 60 * 60 * 1000; // look from 1 day up to 10000 days into the past and future diff --git a/src/plugins/discover/public/application/angular/context_query_state.ts b/src/plugins/discover/public/application/apps/context/services/context_query_state.ts similarity index 93% rename from src/plugins/discover/public/application/angular/context_query_state.ts rename to src/plugins/discover/public/application/apps/context/services/context_query_state.ts index d1626d2be4acc..132b74647f66e 100644 --- a/src/plugins/discover/public/application/angular/context_query_state.ts +++ b/src/plugins/discover/public/application/apps/context/services/context_query_state.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { EsHitRecord } from './context/api/context'; -import { EsHitRecordList } from './context/api/context'; +import { EsHitRecord, EsHitRecordList } from '../../../types'; export interface ContextFetchState { /** diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/apps/context/services/context_state.test.ts similarity index 96% rename from src/plugins/discover/public/application/angular/context_state.test.ts rename to src/plugins/discover/public/application/apps/context/services/context_state.test.ts index ad4051a0c97fe..401bc4d38a352 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context_state.test.ts @@ -9,9 +9,9 @@ import { IUiSettingsClient } from 'kibana/public'; import { getState } from './context_state'; import { createBrowserHistory, History } from 'history'; -import { FilterManager, Filter } from '../../../../data/public'; -import { coreMock } from '../../../../../core/public/mocks'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { FilterManager, Filter } from '../../../../../../data/public'; +import { coreMock } from '../../../../../../../core/public/mocks'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; const setupMock = coreMock.createSetup(); diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/apps/context/services/context_state.ts similarity index 98% rename from src/plugins/discover/public/application/angular/context_state.ts rename to src/plugins/discover/public/application/apps/context/services/context_state.ts index 20cf61dba1578..582ca196e3484 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/apps/context/services/context_state.ts @@ -15,9 +15,9 @@ import { syncStates, withNotifyOnErrors, ReduxLikeStateContainer, -} from '../../../../kibana_utils/public'; -import { esFilters, FilterManager, Filter, SortDirection } from '../../../../data/public'; -import { handleSourceColumnState } from './helpers'; +} from '../../../../../../kibana_utils/public'; +import { esFilters, FilterManager, Filter, SortDirection } from '../../../../../../data/public'; +import { handleSourceColumnState } from '../../../helpers/state_helpers'; export interface AppState { /** diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.test.ts b/src/plugins/discover/public/application/apps/context/services/utils/date_conversion.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/date_conversion.test.ts rename to src/plugins/discover/public/application/apps/context/services/utils/date_conversion.test.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts b/src/plugins/discover/public/application/apps/context/services/utils/date_conversion.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts rename to src/plugins/discover/public/application/apps/context/services/utils/date_conversion.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts b/src/plugins/discover/public/application/apps/context/services/utils/fetch_hits_in_interval.ts similarity index 97% rename from src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts rename to src/plugins/discover/public/application/apps/context/services/utils/fetch_hits_in_interval.ts index 7db76aeb3db7c..feacf940e312a 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts +++ b/src/plugins/discover/public/application/apps/context/services/utils/fetch_hits_in_interval.ts @@ -8,9 +8,9 @@ import { ISearchSource, EsQuerySortValue, SortDirection } from '../../../../../../../data/public'; import { convertTimeValueToIso } from './date_conversion'; -import { EsHitRecordList, EsHitRecord } from '../context'; import { IntervalValue } from './generate_intervals'; import { EsQuerySearchAfter } from './get_es_query_search_after'; +import { EsHitRecord, EsHitRecordList } from '../../../../types'; interface RangeQuery { format: string; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts b/src/plugins/discover/public/application/apps/context/services/utils/generate_intervals.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts rename to src/plugins/discover/public/application/apps/context/services/utils/generate_intervals.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_search_after.ts similarity index 94% rename from src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts rename to src/plugins/discover/public/application/apps/context/services/utils/get_es_query_search_after.ts index c8064202f2c82..721459fee08f8 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_search_after.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { SurrDocType, EsHitRecordList, EsHitRecord } from '../context'; +import { SurrDocType } from '../context'; +import { EsHitRecord, EsHitRecordList } from '../../../../types'; export type EsQuerySearchAfter = [string | number, string | number]; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_sort.ts b/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/get_es_query_sort.ts rename to src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/sorting.test.ts b/src/plugins/discover/public/application/apps/context/services/utils/sorting.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/sorting.test.ts rename to src/plugins/discover/public/application/apps/context/services/utils/sorting.test.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts b/src/plugins/discover/public/application/apps/context/services/utils/sorting.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/sorting.ts rename to src/plugins/discover/public/application/apps/context/services/utils/sorting.ts diff --git a/src/plugins/discover/public/application/components/context_app/utils/constants.ts b/src/plugins/discover/public/application/apps/context/utils/constants.ts similarity index 100% rename from src/plugins/discover/public/application/components/context_app/utils/constants.ts rename to src/plugins/discover/public/application/apps/context/utils/constants.ts diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts similarity index 90% rename from src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts rename to src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts index 1bdf323630080..8d6d81bcaa493 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts +++ b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts @@ -7,25 +7,25 @@ */ import { act, renderHook } from '@testing-library/react-hooks'; -import { setServices, getServices } from '../../../kibana_services'; -import { SortDirection } from '../../../../../data/public'; -import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; -import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common'; -import { DiscoverServices } from '../../../build_services'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { indexPatternsMock } from '../../../__mocks__/index_patterns'; -import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; +import { setServices, getServices } from '../../../../kibana_services'; +import { SortDirection } from '../../../../../../data/public'; +import { createFilterManagerMock } from '../../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../../common'; +import { DiscoverServices } from '../../../../build_services'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; +import { FailureReason, LoadingStatus } from '../services/context_query_state'; import { ContextAppFetchProps, useContextAppFetch } from './use_context_app_fetch'; import { mockAnchorHit, mockPredecessorHits, mockSuccessorHits, -} from './__mocks__/use_context_app_fetch'; +} from '../__mocks__/use_context_app_fetch'; const mockFilterManager = createFilterManagerMock(); -jest.mock('../../angular/context/api/context', () => { - const originalModule = jest.requireActual('../../angular/context/api/context'); +jest.mock('../services/context', () => { + const originalModule = jest.requireActual('../services/context'); return { ...originalModule, fetchContextProvider: () => ({ @@ -39,7 +39,7 @@ jest.mock('../../angular/context/api/context', () => { }; }); -jest.mock('../../angular/context/api/anchor', () => ({ +jest.mock('../services/anchor', () => ({ fetchAnchorProvider: () => (indexPatternId: string) => { if (!indexPatternId) { throw new Error(); diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx similarity index 90% rename from src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx rename to src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx index 5bb23eae3e2e2..fa6a761397335 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx @@ -8,20 +8,21 @@ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { fromPairs } from 'lodash'; -import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common'; -import { DiscoverServices } from '../../../build_services'; -import { fetchAnchorProvider } from '../../angular/context/api/anchor'; -import { EsHitRecord, fetchContextProvider, SurrDocType } from '../../angular/context/api/context'; -import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; -import { IndexPattern, SortDirection } from '../../../../../data/public'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../../common'; +import { DiscoverServices } from '../../../../build_services'; +import { fetchAnchorProvider } from '../services/anchor'; +import { fetchContextProvider, SurrDocType } from '../services/context'; +import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; +import { IndexPattern, SortDirection } from '../../../../../../data/public'; import { ContextFetchState, FailureReason, getInitialContextQueryState, LoadingStatus, -} from '../../angular/context_query_state'; -import { AppState } from '../../angular/context_state'; -import { getFirstSortableField } from '../../angular/context/api/utils/sorting'; +} from '../services/context_query_state'; +import { AppState } from '../services/context_state'; +import { getFirstSortableField } from '../services/utils/sorting'; +import { EsHitRecord } from '../../../types'; const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({ [statusKey]: { value: LoadingStatus.FAILED, error, reason }, diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_state.ts b/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts similarity index 89% rename from src/plugins/discover/public/application/components/context_app/use_context_app_state.ts rename to src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts index a2d64b0ecde07..3e968b5dfb82e 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_state.ts +++ b/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts @@ -8,10 +8,10 @@ import { useEffect, useMemo, useState } from 'react'; import { cloneDeep } from 'lodash'; -import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../common'; -import { IndexPattern } from '../../../../../data/public'; -import { DiscoverServices } from '../../../build_services'; -import { AppState, getState } from '../../angular/context_state'; +import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../../common'; +import { IndexPattern } from '../../../../../../data/public'; +import { DiscoverServices } from '../../../../build_services'; +import { AppState, getState } from '../services/context_state'; export function useContextAppState({ indexPattern, diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx similarity index 97% rename from src/plugins/discover/public/application/components/doc/doc.test.tsx rename to src/plugins/discover/public/application/apps/doc/components/doc.test.tsx index 7367315eae7fb..31ff39ea6b577 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx @@ -13,11 +13,11 @@ import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { Doc, DocProps } from './doc'; -import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; +import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../../common'; const mockSearchApi = jest.fn(); -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let registry: any[] = []; diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/apps/doc/components/doc.tsx similarity index 94% rename from src/plugins/discover/public/application/components/doc/doc.tsx rename to src/plugins/discover/public/application/apps/doc/components/doc.tsx index ed8bcf30d2bd1..b184a1cfd238c 100644 --- a/src/plugins/discover/public/application/components/doc/doc.tsx +++ b/src/plugins/discover/public/application/apps/doc/components/doc.tsx @@ -10,10 +10,10 @@ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; -import { useEsDocSearch } from './use_es_doc_search'; -import { getServices } from '../../../kibana_services'; -import { DocViewer } from '../doc_viewer/doc_viewer'; -import { ElasticRequestState } from './elastic_request_state'; +import { getServices } from '../../../../kibana_services'; +import { DocViewer } from '../../../components/doc_viewer/doc_viewer'; +import { ElasticRequestState } from '../types'; +import { useEsDocSearch } from '../../../services/use_es_doc_search'; export interface DocProps { /** diff --git a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx index 9088464980c26..8398f6255e0f9 100644 --- a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import { DiscoverServices } from '../../../build_services'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; -import { Doc } from '../../components/doc/doc'; +import { Doc } from './components/doc'; import { LoadingIndicator } from '../../components/common/loading_indicator'; import { useIndexPattern } from '../../helpers/use_index_pattern'; diff --git a/src/plugins/discover/public/application/components/doc/elastic_request_state.ts b/src/plugins/discover/public/application/apps/doc/types.ts similarity index 100% rename from src/plugins/discover/public/application/components/doc/elastic_request_state.ts rename to src/plugins/discover/public/application/apps/doc/types.ts diff --git a/src/plugins/discover/public/application/apps/main/components/chart/histogram.scss b/src/plugins/discover/public/application/apps/main/components/chart/histogram.scss index 10bec21a937ad..4224ad43635ed 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/histogram.scss +++ b/src/plugins/discover/public/application/apps/main/components/chart/histogram.scss @@ -7,3 +7,7 @@ height: 100%; width: 100%; } +.dscHistogram__header--partial { + font-weight: $euiFontWeightRegular; + min-width: $euiSize * 12; +} diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts index 9b69a98ca7996..f3ad590ac6ce4 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts @@ -7,7 +7,6 @@ */ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../../../common'; -import { popularizeField } from '../../../../../../application/helpers/popularize_field'; import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, @@ -15,8 +14,9 @@ import { import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, -} from '../../../../../../application/angular/context_state'; +} from '../../../../context/services/context_state'; import { IndexPattern, IndexPatternsContract } from '../../../../../../../../data/public'; +import { popularizeField } from '../../../../../helpers/popularize_field'; /** * Helper function to provide a fallback to a single _source column if the given array of columns diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx index 145053de1f21c..e654d87ea2ba6 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx @@ -99,13 +99,6 @@ describe('DiscoverFieldSearch', () => { expect(badge.text()).toEqual('0'); }); - test('missing switch appears with new fields api', () => { - const component = mountComponent({ ...defaultProps, useNewFieldsApi: true }); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - expect(findTestSubject(component, 'missingSwitch').exists()).toBeTruthy(); - }); - test('change in filters triggers onChange', () => { const onChange = jest.fn(); const component = mountComponent({ ...defaultProps, ...{ onChange } }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx index 4abfa6ecea55a..12b19300ce266 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx @@ -53,18 +53,13 @@ export interface Props { * types for the type filter */ types: string[]; - - /** - * use new fields api - */ - useNewFieldsApi?: boolean; } /** * Component is Discover's side bar to search of available fields * Additionally there's a button displayed that allows the user to show/hide more filter fields */ -export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: Props) { +export function DiscoverFieldSearch({ onChange, value, types }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', }); @@ -92,12 +87,6 @@ export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: missing: true, }); - if (typeof value !== 'string') { - // at initial rendering value is undefined (angular related), this catches the warning - // should be removed once all is react - return null; - } - const filterBtnAriaLabel = isPopoverOpen ? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { defaultMessage: 'Hide field filter settings', diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx index 356c86dded97a..f22d88f2b2150 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx @@ -23,7 +23,7 @@ export interface DiscoverIndexPatternProps { */ onChangeIndexPattern: (id: string) => void; /** - * currently selected index pattern, due to angular issues it's undefined at first rendering + * currently selected index pattern */ selectedIndexPattern: IndexPattern; } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx index 938b0a49b29a7..b42bb4fe09bf1 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx @@ -338,7 +338,6 @@ export function DiscoverSidebar({ onChange={onChangeFieldSearch} value={fieldFilter.name} types={fieldTypes} - useNewFieldsApi={useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx index 2ccaaffd5d835..398fcfbc41140 100644 --- a/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx +++ b/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx @@ -26,7 +26,7 @@ export function SkipBottomButton({ onClick }: SkipBottomButtonProps) { // prevent the anchor to reload the page on click event.preventDefault(); // The destinationId prop cannot be leveraged here as the table needs - // to be updated first (angular logic) + // to be updated firsts onClick(); }} className="dscSkipButton" diff --git a/src/plugins/discover/public/application/apps/main/services/discover_state.ts b/src/plugins/discover/public/application/apps/main/services/discover_state.ts index 6da33e82f06b7..e30af2390d44e 100644 --- a/src/plugins/discover/public/application/apps/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/discover_state.ts @@ -34,7 +34,7 @@ import { migrateLegacyQuery } from '../../../helpers/migrate_legacy_query'; import { DiscoverGridSettings } from '../../../components/discover_grid/types'; import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../../../url_generator'; import { SavedSearch } from '../../../../saved_searches'; -import { handleSourceColumnState } from '../../../angular/helpers'; +import { handleSourceColumnState } from '../../../helpers/state_helpers'; export interface AppState { /** diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts deleted file mode 100644 index 596f105aaa8b5..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ /dev/null @@ -1,18 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ContextApp } from './context_app'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createContextAppLegacy(reactDirective: any) { - return reactDirective(ContextApp, [ - ['indexPattern', { watchDepth: 'reference' }], - ['indexPatternId', { watchDepth: 'reference' }], - ['anchorId', { watchDepth: 'reference' }], - ]); -} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx index b11733c159520..f259d5c5c3658 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx @@ -12,7 +12,7 @@ import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; -import { EsHitRecord } from '../../angular/context/api/context'; +import { EsHitRecord } from '../../types'; /** * Button to expand a given row */ diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index 0dfbdffd175ac..8bea977ece554 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -21,7 +21,7 @@ import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; import { defaultMonacoEditorWidth } from './constants'; -import { EsHitRecord } from '../../angular/context/api/context'; +import { EsHitRecord } from '../../types'; export const getRenderCellValueFn = ( indexPattern: IndexPattern, diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx index ad477041c55d5..a6967cac8cdcc 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx @@ -15,7 +15,7 @@ interface Props { } /** * Responsible for rendering a tab provided by a render function. - * So any other framework can be used (E.g. legacy Angular 3rd party plugin code) + * Any other framework can be used * The provided `render` function is called with a reference to the * component's `HTMLDivElement` as 1st arg and `renderProps` as 2nd arg */ diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx index 4ca53d929eeab..52d9b8316bb09 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx @@ -61,7 +61,7 @@ export class DocViewerTab extends React.Component { } if (render) { - // doc view is provided by a render function, e.g. for legacy Angular code + // doc view is provided by a render function return ; } diff --git a/src/plugins/discover/public/application/components/field_name/field_name.tsx b/src/plugins/discover/public/application/components/field_name/field_name.tsx index a180c35fb2653..7966b611215a7 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.tsx @@ -15,8 +15,6 @@ import { FieldIcon, FieldIconProps } from '../../../../../kibana_react/public'; import { getFieldTypeName } from './field_type_name'; import { IndexPatternField } from '../../../../../data/public'; -// properties fieldType and fieldName are provided in kbn_doc_view -// this should be changed when both components are deangularized interface Props { fieldName: string; fieldType?: string; diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx index 86433e5df6401..625ac93a335ac 100644 --- a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { SourceViewer } from './source_viewer'; -import * as hooks from '../doc/use_es_doc_search'; +import * as hooks from '../../services/use_es_doc_search'; import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { JsonCodeEditorCommon } from '../json_code_editor/json_code_editor_common'; diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx index 94a12c04613a9..1ec595c9d17f2 100644 --- a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx @@ -12,11 +12,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { monaco } from '@kbn/monaco'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useEsDocSearch } from '../doc/use_es_doc_search'; import { JSONCodeEditorCommonMemoized } from '../json_code_editor/json_code_editor_common'; -import { ElasticRequestState } from '../doc/elastic_request_state'; -import { getServices } from '../../../../public/kibana_services'; +import { getServices } from '../../../kibana_services'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; +import { ElasticRequestState } from '../../apps/doc/types'; +import { useEsDocSearch } from '../../services/use_es_doc_search'; interface SourceViewerProps { id: string; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx index a4ab6ec03b40c..e060a0fbae871 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx @@ -38,7 +38,7 @@ import { SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; import * as columnActions from '../apps/main/components/doc_table/actions/columns'; -import { handleSourceColumnState } from '../angular/helpers'; +import { handleSourceColumnState } from '../helpers/state_helpers'; import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapper'; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx index 76b316d575cf2..058794fa59a18 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx @@ -8,10 +8,7 @@ import React from 'react'; -import { - DiscoverGridEmbeddable, - DiscoverGridEmbeddableProps, -} from '../angular/create_discover_grid_directive'; +import { DiscoverGridEmbeddable, DiscoverGridEmbeddableProps } from './saved_search_grid'; import { DiscoverDocTableEmbeddable } from '../apps/main/components/doc_table/create_doc_table_embeddable'; import { DocTableEmbeddableProps } from '../apps/main/components/doc_table/doc_table_embeddable'; import { SearchProps } from './saved_search_embeddable'; diff --git a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/embeddable/saved_search_grid.tsx similarity index 62% rename from src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx rename to src/plugins/discover/public/application/embeddable/saved_search_grid.tsx index b79936bd6f385..0dce6d2b3abe8 100644 --- a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_grid.tsx @@ -44,28 +44,3 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { ); } - -/** - * this is just needed for the embeddable - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDiscoverGridDirective(reactDirective: any) { - return reactDirective(DiscoverGridEmbeddable, [ - ['columns', { watchDepth: 'collection' }], - ['indexPattern', { watchDepth: 'reference' }], - ['isLoading', { watchDepth: 'reference' }], - ['onAddColumn', { watchDepth: 'reference', wrapApply: false }], - ['onFilter', { watchDepth: 'reference', wrapApply: false }], - ['onRemoveColumn', { watchDepth: 'reference', wrapApply: false }], - ['onSetColumns', { watchDepth: 'reference', wrapApply: false }], - ['onSort', { watchDepth: 'reference', wrapApply: false }], - ['rows', { watchDepth: 'collection' }], - ['sampleSize', { watchDepth: 'reference' }], - ['searchDescription', { watchDepth: 'reference' }], - ['searchTitle', { watchDepth: 'reference' }], - ['settings', { watchDepth: 'reference' }], - ['showTimeCol', { watchDepth: 'value' }], - ['sort', { watchDepth: 'value' }], - ['className', { watchDepth: 'value' }], - ]); -} diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index 360844976284e..b314d9ab55cd1 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { auto } from 'angular'; import { i18n } from '@kbn/i18n'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { getServices } from '../../kibana_services'; @@ -30,8 +29,6 @@ interface StartServices { export class SearchEmbeddableFactory implements EmbeddableFactoryDefinition { public readonly type = SEARCH_EMBEDDABLE_TYPE; - private $injector: auto.IInjectorService | null; - private getInjector: () => Promise | null; public readonly savedObjectMetaData = { name: i18n.translate('discover.savedSearch.savedObjectName', { defaultMessage: 'Saved search', @@ -40,13 +37,7 @@ export class SearchEmbeddableFactory getIconForSavedObject: () => 'discoverApp', }; - constructor( - private getStartServices: () => Promise, - getInjector: () => Promise - ) { - this.$injector = null; - this.getInjector = getInjector; - } + constructor(private getStartServices: () => Promise) {} public canCreateNew() { return false; @@ -67,10 +58,6 @@ export class SearchEmbeddableFactory input: Partial & { id: string; timeRange: TimeRange }, parent?: Container ): Promise => { - if (!this.$injector) { - this.$injector = await this.getInjector(); - } - const filterManager = getServices().filterManager; const url = await getServices().getSavedSearchUrlById(savedObjectId); diff --git a/src/plugins/discover/public/application/angular/helpers/state_helpers.ts b/src/plugins/discover/public/application/helpers/state_helpers.ts similarity index 95% rename from src/plugins/discover/public/application/angular/helpers/state_helpers.ts rename to src/plugins/discover/public/application/helpers/state_helpers.ts index bcd3aedd3f034..fd17ec9516ab5 100644 --- a/src/plugins/discover/public/application/angular/helpers/state_helpers.ts +++ b/src/plugins/discover/public/application/helpers/state_helpers.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient } from 'kibana/public'; import { isEqual } from 'lodash'; -import { SEARCH_FIELDS_FROM_SOURCE, DEFAULT_COLUMNS_SETTING } from '../../../../common'; +import { SEARCH_FIELDS_FROM_SOURCE, DEFAULT_COLUMNS_SETTING } from '../../../common'; /** * Makes sure the current state is not referencing the source column when using the fields api diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx b/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx index c9e1899aff8de..565e9a04e7afe 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx @@ -11,7 +11,7 @@ import { useDataGridColumns } from './use_data_grid_columns'; import { indexPatternMock } from '../../__mocks__/index_pattern'; import { configMock } from '../../__mocks__/config'; import { indexPatternsMock } from '../../__mocks__/index_patterns'; -import { AppState } from '../angular/context_state'; +import { AppState } from '../apps/context/services/context_state'; import { Capabilities } from '../../../../../core/types'; describe('useDataGridColumns', () => { diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts index 8a28369d1f5f2..66e8889bcb062 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -17,7 +17,7 @@ import { import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, -} from '../angular/context_state'; +} from '../apps/context/services/context_state'; import { getStateColumnActions } from '../apps/main/components/doc_table/actions/columns'; interface UseDataGridColumnsProps { diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover/public/application/index.scss deleted file mode 100644 index 3c24d4f51de2e..0000000000000 --- a/src/plugins/discover/public/application/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'angular/index'; diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx similarity index 96% rename from src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx rename to src/plugins/discover/public/application/services/use_es_doc_search.test.tsx index 1ff623dc0e317..3fcb193c5f6f3 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx @@ -8,15 +8,15 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { buildSearchBody, useEsDocSearch } from './use_es_doc_search'; -import { DocProps } from './doc'; import { Observable } from 'rxjs'; -import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; import { IndexPattern } from 'src/plugins/data/common'; -import { ElasticRequestState } from './elastic_request_state'; +import { DocProps } from '../apps/doc/components/doc'; +import { ElasticRequestState } from '../apps/doc/types'; +import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../common'; const mockSearchResult = new Observable(); -jest.mock('../../../kibana_services', () => ({ +jest.mock('../../kibana_services', () => ({ getServices: () => ({ data: { search: { diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/services/use_es_doc_search.ts similarity index 91% rename from src/plugins/discover/public/application/components/doc/use_es_doc_search.ts rename to src/plugins/discover/public/application/services/use_es_doc_search.ts index ce039aeadc063..a2f0cd6f8442b 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/services/use_es_doc_search.ts @@ -8,11 +8,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { estypes } from '@elastic/elasticsearch'; -import { getServices, IndexPattern } from '../../../kibana_services'; -import { DocProps } from './doc'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; -import { ElasticRequestState } from './elastic_request_state'; +import { IndexPattern } from '../../../../data/common'; +import { DocProps } from '../apps/doc/components/doc'; +import { ElasticRequestState } from '../apps/doc/types'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { getServices } from '../../kibana_services'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; type RequestBody = Pick; diff --git a/src/plugins/discover/public/application/types.ts b/src/plugins/discover/public/application/types.ts index 798e0f350cc5f..a28c5bbc89aed 100644 --- a/src/plugins/discover/public/application/types.ts +++ b/src/plugins/discover/public/application/types.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; export enum FetchStatus { UNINITIALIZED = 'uninitialized', @@ -13,3 +14,13 @@ export enum FetchStatus { COMPLETE = 'complete', ERROR = 'error', } + +export type EsHitRecord = Required< + Pick +> & { + _source?: Record; + _score?: number; + // note that this a special property for Discover Context, to determine the anchor record + isAnchor?: boolean; +}; +export type EsHitRecordList = EsHitRecord[]; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index c8e641088afad..1efae6cd2c0ec 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -8,7 +8,6 @@ import { History } from 'history'; -import type { auto } from 'angular'; import { Capabilities, ChromeStart, @@ -59,19 +58,17 @@ export interface DiscoverServices { toastNotifications: ToastsStart; getSavedSearchById: (id?: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; - getEmbeddableInjector: () => Promise; uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; indexPatternFieldEditor: IndexPatternFieldEditorStart; http: HttpStart; } -export async function buildServices( +export function buildServices( core: CoreStart, plugins: DiscoverStartPlugins, - context: PluginInitializerContext, - getEmbeddableInjector: () => Promise -): Promise { + context: PluginInitializerContext +): DiscoverServices { const services = { savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, @@ -88,7 +85,6 @@ export async function buildServices( docLinks: core.docLinks, theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, - getEmbeddableInjector, getSavedSearchById: async (id?: string) => savedObjectService.get(id), getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), history: getHistory, diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index 11bc61c9b261a..1e92c0e4c2f1d 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -15,27 +15,9 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { search } from '../../data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; -let angularModule: ng.IModule | null = null; let services: DiscoverServices | null = null; let uiActions: UiActionsStart; -/** - * set bootstrapped inner angular module - */ -export function setAngularModule(module: ng.IModule) { - angularModule = module; -} - -/** - * get boostrapped inner angular module - */ -export function getAngularModule(): ng.IModule { - if (!angularModule) { - throw new Error('Discover angular module not yet available'); - } - return angularModule; -} - export function getServices(): DiscoverServices { if (!services) { throw new Error('Discover services are not yet available'); diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 65fc3ce2a82fa..c43c759c5d344 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -8,10 +8,8 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import angular, { auto } from 'angular'; import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; - import { AppMountParameters, AppUpdater, @@ -40,14 +38,12 @@ import { DocViewerTable } from './application/components/table/table'; import { setDocViewsRegistry, setUrlTracker, - setAngularModule, setServices, setHeaderActionMenuMounter, setUiActions, setScopedHistory, getScopedHistory, syncHistoryLocations, - getServices, } from './kibana_services'; import { createSavedSearchesLoader } from './saved_searches'; import { registerFeature } from './register_feature'; @@ -78,7 +74,6 @@ export interface DiscoverSetup { docViews: { /** * Add new doc view shown along with table view and json view in the details of each document in Discover. - * Both react and angular doc views are supported. * @param docViewRaw */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; @@ -121,7 +116,7 @@ export interface DiscoverStart { savedSearchLoader: SavedObjectLoader; /** - * @deprecated Use URL locator instead. URL generaotr will be removed. + * @deprecated Use URL locator instead. URL generator will be removed. */ readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; @@ -189,13 +184,9 @@ export interface DiscoverStartPlugins { indexPatternFieldEditor: IndexPatternFieldEditorStart; } -const innerAngularName = 'app/discover'; -const embeddableAngularName = 'app/discoverEmbeddable'; - /** * Contains Discover, one of the oldest parts of Kibana - * There are 2 kinds of Angular bootstrapped for rendering, additionally to the main Angular - * Discover provides embeddables, those contain a slimmer Angular + * Discover provides embeddables for Dashboards */ export class DiscoverPlugin implements Plugin { @@ -203,10 +194,7 @@ export class DiscoverPlugin private appStateUpdater = new BehaviorSubject(() => ({})); private docViewsRegistry: DocViewsRegistry | null = null; - private embeddableInjector: auto.IInjectorService | null = null; private stopUrlTracking: (() => void) | undefined = undefined; - private servicesInitialized: boolean = false; - private innerAngularInitialized: boolean = false; /** * @deprecated @@ -214,13 +202,6 @@ export class DiscoverPlugin private urlGenerator?: DiscoverStart['urlGenerator']; private locator?: DiscoverAppLocator; - /** - * why are those functions public? they are needed for some mocha tests - * can be removed once all is Jest - */ - public initializeInnerAngular?: () => void; - public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; - setup( core: CoreSetup, plugins: DiscoverSetupPlugins @@ -326,12 +307,7 @@ export class DiscoverPlugin defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.kibana, mount: async (params: AppMountParameters) => { - if (!this.initializeServices) { - throw Error('Discover plugin method initializeServices is undefined'); - } - if (!this.initializeInnerAngular) { - throw Error('Discover plugin method initializeInnerAngular is undefined'); - } + const [, depsStart] = await core.getStartServices(); setScopedHistory(params.history); setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); @@ -341,17 +317,11 @@ export class DiscoverPlugin const unlistenParentHistory = params.history.listen(() => { window.dispatchEvent(new HashChangeEvent('hashchange')); }); - const { - plugins: { data: dataStart }, - } = await this.initializeServices(); - await this.initializeInnerAngular(); - // make sure the index pattern list is up to date - await dataStart.indexPatterns.clearCache(); + await depsStart.data.indexPatterns.clearCache(); const { renderApp } = await import('./application/application'); - params.element.classList.add('dscAppWrapper'); - const unmount = await renderApp(innerAngularName, params.element); + const unmount = await renderApp('discover', params.element); return () => { params.element.classList.remove('dscAppWrapper'); unlistenParentHistory(); @@ -400,42 +370,13 @@ export class DiscoverPlugin start(core: CoreStart, plugins: DiscoverStartPlugins) { // we need to register the application service at setup, but to render it // there are some start dependencies necessary, for this reason - // initializeInnerAngular + initializeServices are assigned at start and used + // initializeServices are assigned at start and used // when the application/embeddable is mounted - this.initializeInnerAngular = async () => { - if (this.innerAngularInitialized) { - return; - } - // this is used by application mount and tests - const { getInnerAngularModule } = await import('./application/angular/get_inner_angular'); - await plugins.kibanaLegacy.loadAngularBootstrap(); - const module = getInnerAngularModule( - innerAngularName, - core, - plugins, - this.initializerContext - ); - setAngularModule(module); - this.innerAngularInitialized = true; - }; setUiActions(plugins.uiActions); - this.initializeServices = async () => { - if (this.servicesInitialized) { - return { core, plugins }; - } - const services = await buildServices( - core, - plugins, - this.initializerContext, - this.getEmbeddableInjector - ); - setServices(services); - this.servicesInitialized = true; - - return { core, plugins }; - }; + const services = buildServices(core, plugins, this.initializerContext); + setServices(services); return { urlGenerator: this.urlGenerator, @@ -453,14 +394,7 @@ export class DiscoverPlugin } } - /** - * register embeddable with a slimmer embeddable version of inner angular - */ private registerEmbeddable(core: CoreSetup, plugins: DiscoverSetupPlugins) { - if (!this.getEmbeddableInjector) { - throw Error('Discover plugin method getEmbeddableInjector is undefined'); - } - const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); return { @@ -469,25 +403,7 @@ export class DiscoverPlugin }; }; - const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector); + const factory = new SearchEmbeddableFactory(getStartServices); plugins.embeddable.registerEmbeddableFactory(factory.type, factory); } - - private getEmbeddableInjector = async () => { - if (!this.embeddableInjector) { - if (!this.initializeServices) { - throw Error('Discover plugin getEmbeddableInjector: initializeServices is undefined'); - } - const { core, plugins } = await this.initializeServices(); - await getServices().kibanaLegacy.loadAngularBootstrap(); - const { getInnerAngularModuleEmbeddable } = await import( - './application/angular/get_inner_angular' - ); - getInnerAngularModuleEmbeddable(embeddableAngularName, core, plugins); - const mountpoint = document.createElement('div'); - this.embeddableInjector = angular.bootstrap(mountpoint, [embeddableAngularName]); - } - - return this.embeddableInjector; - }; } diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 0dafd8073e9c8..15c2093580a7f 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -34,45 +34,7 @@ exports[`InspectorPanel should render as expected 1`] = ` "closed": false, "hasError": false, "isStopped": false, - "observers": Array [ - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - ], + "observers": Array [], "thrownError": null, }, }, @@ -235,45 +197,7 @@ exports[`InspectorPanel should render as expected 1`] = ` "closed": false, "hasError": false, "isStopped": false, - "observers": Array [ - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - ], + "observers": Array [], "thrownError": null, }, }, @@ -474,102 +398,25 @@ exports[`InspectorPanel should render as expected 1`] = `
- } + > + -
- } - > - -

- View 1 -

-
-
-
-
+

+ View 1 +

+ +
diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx index af59516a50e43..3c6e3c079c8ec 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -22,7 +22,7 @@ import { ApplicationStart, HttpStart, IUiSettingsClient } from 'kibana/public'; import { InspectorViewDescription } from '../types'; import { Adapters } from '../../common'; import { InspectorViewChooser } from './inspector_view_chooser'; -import { KibanaContextProvider, RedirectAppLinks } from '../../../kibana_react/public'; +import { KibanaContextProvider } from '../../../kibana_react/public'; import { SharePluginStart } from '../../../share/public'; function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) { @@ -135,9 +135,7 @@ export class InspectorPanel extends Component - - {this.renderSelectedPanel()} - + {this.renderSelectedPanel()} ); diff --git a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx index 12946866f3ec0..36f2f8534399f 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx @@ -6,11 +6,16 @@ * Side Public License, v 1. */ +// We want to allow both right-clicking to open in a new tab and clicking through +// the "Open in Console" link. We could use `RedirectAppLinks` at the top level +// but that inserts a div which messes up the layout of the inspector. +/* eslint-disable @elastic/eui/href-or-on-click */ + import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; import { compressToEncodedURIComponent } from 'lz-string'; -import React from 'react'; +import React, { useCallback } from 'react'; import { CodeEditor, useKibana } from '../../../../../../kibana_react/public'; import { InspectorPluginStartDeps } from '../../../../plugin'; @@ -23,8 +28,8 @@ const copyToClipboardLabel = i18n.translate('inspector.requests.copyToClipboardL defaultMessage: 'Copy to clipboard', }); -const openInDevToolsLabel = i18n.translate('inspector.requests.openInDevToolsLabel', { - defaultMessage: 'Open in Dev Tools', +const openInConsoleLabel = i18n.translate('inspector.requests.openInConsoleLabel', { + defaultMessage: 'Open in Console', }); /** @@ -32,12 +37,18 @@ const openInDevToolsLabel = i18n.translate('inspector.requests.openInDevToolsLab */ export const RequestCodeViewer = ({ indexPattern, json }: RequestCodeViewerProps) => { const { services } = useKibana(); + + const navigateToUrl = services.application?.navigateToUrl; const canShowDevTools = services.application?.capabilities?.dev_tools.show; const devToolsDataUri = compressToEncodedURIComponent(`GET ${indexPattern}/_search\n${json}`); const devToolsHref = services.share.url.locators .get('CONSOLE_APP_LOCATOR') ?.useUrl({ loadFrom: `data:text/plain,${devToolsDataUri}` }); const shouldShowDevToolsLink = !!(indexPattern && canShowDevTools); + const handleDevToolsLinkClick = useCallback( + () => devToolsHref && navigateToUrl && navigateToUrl(devToolsHref), + [devToolsHref, navigateToUrl] + ); return ( - {openInDevToolsLabel} + {openInConsoleLabel} )} diff --git a/src/plugins/vis_types/xy/public/chart_splitter.tsx b/src/plugins/vis_types/xy/public/chart_splitter.tsx index 43c105b85f194..38f1b7e94f0b2 100644 --- a/src/plugins/vis_types/xy/public/chart_splitter.tsx +++ b/src/plugins/vis_types/xy/public/chart_splitter.tsx @@ -8,11 +8,17 @@ import React from 'react'; import { Accessor, AccessorFn, GroupBy, GroupBySort, SmallMultiples } from '@elastic/charts'; +import { DatatableColumn } from '../../../expressions/public'; interface ChartSplitterProps { splitColumnAccessor?: Accessor | AccessorFn; splitRowAccessor?: Accessor | AccessorFn; - sort?: GroupBySort; + splitDimension?: DatatableColumn; +} + +interface SplitDimensionParams { + order?: string; + orderBy?: string; } const CHART_SPLITTER_ID = '__chart_splitter__'; @@ -20,9 +26,14 @@ const CHART_SPLITTER_ID = '__chart_splitter__'; export const ChartSplitter = ({ splitColumnAccessor, splitRowAccessor, - sort, -}: ChartSplitterProps) => - splitColumnAccessor || splitRowAccessor ? ( + splitDimension, +}: ChartSplitterProps) => { + let sort: GroupBySort = 'alphaDesc'; + if (splitDimension?.meta?.params?.id === 'terms') { + const params = splitDimension?.meta?.sourceParams?.params as SplitDimensionParams; + sort = params?.order === 'asc' ? 'alphaAsc' : 'alphaDesc'; + } + return splitColumnAccessor || splitRowAccessor ? ( <> ) : null; +}; diff --git a/src/plugins/vis_types/xy/public/vis_component.tsx b/src/plugins/vis_types/xy/public/vis_component.tsx index 141174194f1bc..3e4f3e1ffe294 100644 --- a/src/plugins/vis_types/xy/public/vis_component.tsx +++ b/src/plugins/vis_types/xy/public/vis_component.tsx @@ -331,6 +331,13 @@ const VisComponent = (props: VisComponentProps) => { ), [getSeriesName, legendPosition, props.uiState, setColor, visParams.palette.name] ); + + const splitChartDimension = visParams.dimensions.splitColumn + ? visData.columns[visParams.dimensions.splitColumn[0].accessor] + : visParams.dimensions.splitRow + ? visData.columns[visParams.dimensions.splitRow[0].accessor] + : undefined; + return (
{ { @@ -32,6 +33,13 @@ const byValueHideTSVBLastValueIndicator = (state: SerializableRecord) => { }; }; +const byValueAddDropLastBucketIntoTSVBModel = (state: SerializableRecord) => { + return { + ...state, + savedVis: commonAddDropLastBucketIntoTSVBModel(state.savedVis), + }; +}; + const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: SerializableRecord) => { return { ...state, @@ -72,7 +80,12 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => { byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel )(state), '7.14.0': (state) => - flow(byValueAddEmptyValueColorRule, byValueMigrateVislibPie, byValueMigrateTagcloud)(state), + flow( + byValueAddEmptyValueColorRule, + byValueMigrateVislibPie, + byValueMigrateTagcloud, + byValueAddDropLastBucketIntoTSVBModel + )(state), }, }; }; diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts index 17b1470a40062..2503ac2c54b12 100644 --- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts @@ -20,6 +20,27 @@ export const commonAddSupportOfDualIndexSelectionModeInTSVB = (visState: any) => return visState; }; +export const commonAddDropLastBucketIntoTSVBModel = (visState: any) => { + if (visState && visState.type === 'metrics') { + return { + ...visState, + params: { + ...visState.params, + series: visState.params?.series?.map((s: any) => + s.override_index_pattern + ? { + ...s, + series_drop_last_bucket: s.series_drop_last_bucket ?? 1, + } + : s + ), + drop_last_bucket: visState.params.drop_last_bucket ?? 1, + }, + }; + } + return visState; +}; + export const commonHideTSVBLastValueIndicator = (visState: any) => { if (visState && visState.type === 'metrics' && visState.params.type !== 'timeseries') { return { diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index 869a9add89066..00c7e26715e6c 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -2115,6 +2115,87 @@ describe('migration visualization', () => { }); }); + describe('7.14.0 tsvb - add drop last bucket into TSVB model', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.14.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const createTestDocWithType = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: `{ + "type":"metrics", + "params": ${JSON.stringify(params)} + }`, + }, + }); + + it('should add "drop_last_bucket" into model if it not exist', () => { + const params = {}; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + expect(migratedParams).toMatchInlineSnapshot(` + Object { + "drop_last_bucket": 1, + } + `); + }); + + it('should add "series_drop_last_bucket" into model if it not exist', () => { + const params = { + series: [ + { + override_index_pattern: 1, + }, + { + override_index_pattern: 1, + }, + { override_index_pattern: 0 }, + {}, + { + override_index_pattern: 1, + series_drop_last_bucket: 0, + }, + { + override_index_pattern: 1, + series_drop_last_bucket: 1, + }, + ], + }; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + expect(migratedParams.series).toMatchInlineSnapshot(` + Array [ + Object { + "override_index_pattern": 1, + "series_drop_last_bucket": 1, + }, + Object { + "override_index_pattern": 1, + "series_drop_last_bucket": 1, + }, + Object { + "override_index_pattern": 0, + }, + Object {}, + Object { + "override_index_pattern": 1, + "series_drop_last_bucket": 0, + }, + Object { + "override_index_pattern": 1, + "series_drop_last_bucket": 1, + }, + ] + `); + }); + }); + describe('7.14.0 update pie visualization defaults', () => { const migrate = (doc: any) => visualizationSavedObjectTypeMigrations['7.14.0']( diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index 1f50e26ea9ec1..fd08ecd748668 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -18,6 +18,7 @@ import { commonMigrateVislibPie, commonAddEmptyValueColorRule, commonMigrateTagCloud, + commonAddDropLastBucketIntoTSVBModel, } from './visualization_common_migrations'; const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { @@ -945,6 +946,23 @@ const hideTSVBLastValueIndicator: SavedObjectMigrationFn = (doc) => { return doc; }; +const addDropLastBucketIntoTSVBModel: SavedObjectMigrationFn = (doc) => { + try { + const visState = JSON.parse(doc.attributes.visState); + const newVisState = commonAddDropLastBucketIntoTSVBModel(visState); + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(newVisState), + }, + }; + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + return doc; +}; + const removeDefaultIndexPatternAndTimeFieldFromTSVBModel: SavedObjectMigrationFn = ( doc ) => { @@ -1100,6 +1118,7 @@ export const visualizationSavedObjectTypeMigrations = { addEmptyValueColorRule, migrateVislibPie, migrateTagCloud, - replaceIndexPatternReference + replaceIndexPatternReference, + addDropLastBucketIntoTSVBModel ), }; diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index a29d8825068af..7c093b5a9640a 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -45,19 +45,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(tableData).to.be(EXPECTED); }); - it('should display drilldown urls', async () => { - const baseURL = 'http://elastic.co/foo/'; - - await visualBuilder.clickPanelOptions('table'); - await visualBuilder.setDrilldownUrl(`${baseURL}{{key}}`); - - await retry.try(async () => { - const links = await findService.allByCssSelector(`a[href="${baseURL}ios"]`); - - expect(links.length).to.be(1); - }); - }); - it('should display correct values on changing metrics aggregation', async () => { const EXPECTED = 'OS Cardinality\nwin 8 12\nwin xp 9\nwin 7 8\nios 5\nosx 3'; @@ -134,6 +121,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const tableData = await visualBuilder.getViewTable(); expect(tableData).to.be(EXPECTED); }); + + it('should display drilldown urls', async () => { + const baseURL = 'http://elastic.co/foo/'; + + await visualBuilder.clickPanelOptions('table'); + await visualBuilder.setDrilldownUrl(`${baseURL}{{key}}`); + + await retry.try(async () => { + const links = await findService.allByCssSelector(`a[href="${baseURL}ios"]`); + + expect(links.length).to.be(1); + }); + }); }); }); } diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index 0384a65dcb389..00985df8ab834 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -15,7 +15,7 @@ export const createCaseCommentSavedObjectType = ({ migrationDeps: CreateCommentsMigrationsDeps; }): SavedObjectsType => ({ name: CASE_COMMENT_SAVED_OBJECT, - hidden: false, + hidden: true, namespaceType: 'single', mappings: { properties: { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts index 751f9e11f7370..339a30f50d631 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { flow, mapValues } from 'lodash'; +import { mapValues } from 'lodash'; import { LensServerPluginSetup } from '../../../../lens/server'; import { @@ -148,49 +148,43 @@ export const createCommentsMigrations = ( ) as MigrateFunctionsObject; const commentsMigrations = { - '7.11.0': flow( - ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - type: CommentType.user, - }, - references: doc.references || [], - }; - } - ), - '7.12.0': flow( - ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { + '7.11.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { ...doc.attributes, - associationType: AssociationType.case, - }; - - // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are - // introduced in 7.12. - if (doc.attributes.type === CommentType.alert) { - attributes = { ...attributes, rule: { id: null, name: null } }; - } - - return { - ...doc, - attributes, - references: doc.references || [], - }; - } - ), - '7.14.0': flow( - ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); + type: CommentType.user, + }, + references: doc.references || [], + }; + }, + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { + ...doc.attributes, + associationType: AssociationType.case, + }; + + // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are + // introduced in 7.12. + if (doc.attributes.type === CommentType.alert) { + attributes = { ...attributes, rule: { id: null, name: null } }; } - ), + + return { + ...doc, + attributes, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, }; return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts index addee72ae4bd2..b2dc665fc2b85 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.test.ts @@ -13,8 +13,8 @@ import { } from '../../../../../__mocks__/kea_logic'; import '../../../../__mocks__/engine_logic.mock'; -jest.mock('../../crawler_overview_logic', () => ({ - CrawlerOverviewLogic: { +jest.mock('../../crawler_logic', () => ({ + CrawlerLogic: { actions: { onReceiveCrawlerData: jest.fn(), }, @@ -28,7 +28,7 @@ jest.mock('./utils', () => ({ import { nextTick } from '@kbn/test/jest'; -import { CrawlerOverviewLogic } from '../../crawler_overview_logic'; +import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerDomain } from '../../types'; import { AddDomainLogic, AddDomainLogicValues } from './add_domain_logic'; @@ -310,7 +310,7 @@ describe('AddDomainLogic', () => { AddDomainLogic.actions.submitNewDomain(); await nextTick(); - expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith({ + expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith({ domains: [], }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts index 17b16a55a8fa3..23bc147a7291d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/add_domain_logic.ts @@ -17,7 +17,7 @@ import { KibanaLogic } from '../../../../../shared/kibana'; import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../../routes'; import { EngineLogic, generateEnginePath } from '../../../engine'; -import { CrawlerOverviewLogic } from '../../crawler_overview_logic'; +import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerDataFromServer, CrawlerDomain, @@ -262,7 +262,7 @@ export const AddDomainLogic = kea { export const domainValidationStateToPanelColor = ( state: CrawlerDomainValidationStepState -): 'success' | 'danger' | 'subdued' => { +): 'success' | 'warning' | 'danger' | 'subdued' => { switch (state) { case 'valid': return 'success'; + case 'warning': + return 'warning'; case 'invalid': return 'danger'; default: diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.test.tsx index 2c27e99e02ef6..8bb82f93e3ec4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.test.tsx @@ -20,6 +20,12 @@ describe('ValidationStateIcon', () => { expect(wrapper.find(EuiIcon).prop('color')).toEqual('success'); }); + it('shows a warning icon when warning', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiIcon).prop('color')).toEqual('warning'); + }); + it('shows a danger icon when invalid', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.tsx index 0c3d5329c47b7..3d85455ec7a8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_state_icon.tsx @@ -14,7 +14,14 @@ import { CrawlerDomainValidationStepState } from '../../types'; export const ValidationStateIcon: React.FC<{ state: CrawlerDomainValidationStepState }> = ({ state, }) => { - if (state === 'valid') return ; - if (state === 'invalid') return ; - return ; + switch (state) { + case 'valid': + return ; + case 'warning': + return ; + case 'invalid': + return ; + default: + return ; + } }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.test.tsx index a02a29c9854dd..c022b09d4638c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.test.tsx @@ -19,12 +19,15 @@ describe('ValidationStepPanel', () => { const wrapper = shallow( ); + it('passed the correct color to the EuiPanel', () => { expect(wrapper.find(EuiPanel).prop('color')).toEqual('success'); }); + it('contains a validation state icon', () => { expect(wrapper.find(ValidationStateIcon)).toHaveLength(1); }); + it('renders a label', () => { expect(wrapper.find('h3').text()).toEqual('Initial validation'); }); @@ -32,11 +35,26 @@ describe('ValidationStepPanel', () => { describe('invalid messages and actions', () => { const errorMessage = 'Error message'; const action =
; + it('displays the passed error message and action is invalid', () => { const wrapper = shallow( + ); + expect(wrapper.find('[data-test-subj="errorMessage"]').dive().text()).toContain( + 'Error message' + ); + expect(wrapper.find('[data-test-subj="action"]')).toHaveLength(1); + }); + + it('displays the passed error message and action when state is warning', () => { + const wrapper = shallow( + ); @@ -45,11 +63,12 @@ describe('ValidationStepPanel', () => { ); expect(wrapper.find('[data-test-subj="action"]')).toHaveLength(1); }); - it('does not display the passed error message or action when state is not invalid', () => { + + it('does not display the passed error message or action when state is loading', () => { const wrapper = shallow( ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.tsx index 8d3faed1fbc58..804c2d86ca099 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/add_domain/validation_step_panel.tsx @@ -25,11 +25,13 @@ export const ValidationStepPanel: React.FC = ({ label, action, }) => { + const showErrorMessage = step.state === 'invalid' || step.state === 'warning'; + return ( - + @@ -37,7 +39,7 @@ export const ValidationStepPanel: React.FC = ({ - {step.state === 'invalid' && ( + {showErrorMessage && ( <>

{step.message}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx index c9a540b9bf72b..13a7c641822b9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx @@ -16,38 +16,12 @@ import { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui'; import { mountWithIntl } from '../../../../test_helpers'; -import { - CrawlerDomain, - CrawlerPolicies, - CrawlerRules, - CrawlerStatus, - CrawlRequest, -} from '../types'; +import { CrawlerStatus, CrawlRequest } from '../types'; import { CrawlRequestsTable } from './crawl_requests_table'; -const values: { domains: CrawlerDomain[]; crawlRequests: CrawlRequest[] } = { - // CrawlerOverviewLogic - domains: [ - { - id: '507f1f77bcf86cd799439011', - createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', - url: 'elastic.co', - documentCount: 13, - sitemaps: [], - entryPoints: [], - crawlRules: [], - defaultCrawlRule: { - id: '-', - policy: CrawlerPolicies.allow, - rule: CrawlerRules.regex, - pattern: '.*', - }, - deduplicationEnabled: false, - deduplicationFields: ['title'], - availableDeduplicationFields: ['title', 'description'], - }, - ], +const values: { crawlRequests: CrawlRequest[] } = { + // CrawlerLogic crawlRequests: [ { id: '618d0e66abe97bc688328900', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx index 19b1a543ad207..8a2b08878ff78 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.tsx @@ -13,7 +13,7 @@ import { EuiBasicTable, EuiEmptyPrompt, EuiTableFieldDataColumnType } from '@ela import { i18n } from '@kbn/i18n'; -import { CrawlerOverviewLogic } from '../crawler_overview_logic'; +import { CrawlerLogic } from '../crawler_logic'; import { CrawlRequest, readableCrawlerStatuses } from '../types'; import { CustomFormattedTimestamp } from './custom_formatted_timestamp'; @@ -53,7 +53,7 @@ const columns: Array> = [ ]; export const CrawlRequestsTable: React.FC = () => { - const { crawlRequests } = useValues(CrawlerOverviewLogic); + const { crawlRequests } = useValues(CrawlerLogic); return ( { - const { mostRecentCrawlRequestStatus } = useValues(CrawlerOverviewLogic); + const { mostRecentCrawlRequestStatus } = useValues(CrawlerLogic); if ( mostRecentCrawlRequestStatus === CrawlerStatus.Running || mostRecentCrawlRequestStatus === CrawlerStatus.Starting || diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx index c1b8ad2073444..c02e45f02c407 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx @@ -13,14 +13,14 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CrawlerOverviewLogic } from '../../crawler_overview_logic'; +import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerStatus } from '../../types'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; export const CrawlerStatusIndicator: React.FC = () => { - const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerOverviewLogic); - const { startCrawl, stopCrawl } = useActions(CrawlerOverviewLogic); + const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerLogic); + const { startCrawl, stopCrawl } = useActions(CrawlerLogic); const disabledButton = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx index 9dd0a518f233b..7214eace25e2d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx @@ -20,6 +20,7 @@ import { KibanaLogic } from '../../../../shared/kibana'; import { AppLogic } from '../../../app_logic'; import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../routes'; import { generateEnginePath } from '../../engine'; +import { CrawlerLogic } from '../crawler_logic'; import { CrawlerOverviewLogic } from '../crawler_overview_logic'; import { CrawlerDomain } from '../types'; @@ -28,7 +29,7 @@ import { getDeleteDomainConfirmationMessage } from '../utils'; import { CustomFormattedTimestamp } from './custom_formatted_timestamp'; export const DomainsTable: React.FC = () => { - const { domains } = useValues(CrawlerOverviewLogic); + const { domains } = useValues(CrawlerLogic); const { deleteDomain } = useActions(CrawlerOverviewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts new file mode 100644 index 0000000000000..6dbc84451280f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -0,0 +1,411 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockHttpValues, + mockFlashMessageHelpers, +} from '../../../__mocks__/kea_logic'; +import '../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { CrawlerLogic, CrawlerValues } from './crawler_logic'; +import { + CrawlerData, + CrawlerDataFromServer, + CrawlerPolicies, + CrawlerRules, + CrawlerStatus, + CrawlRequest, + CrawlRule, +} from './types'; +import { crawlerDataServerToClient } from './utils'; + +const DEFAULT_VALUES: CrawlerValues = { + crawlRequests: [], + dataLoading: true, + domains: [], + mostRecentCrawlRequestStatus: CrawlerStatus.Success, + timeoutId: null, +}; + +const DEFAULT_CRAWL_RULE: CrawlRule = { + id: '-', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.regex, + pattern: '.*', +}; + +const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { + domains: [ + { + id: '507f1f77bcf86cd799439011', + name: 'elastic.co', + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 13, + sitemaps: [], + entry_points: [], + crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], + }, + ], +}; + +const MOCK_CLIENT_CRAWLER_DATA = crawlerDataServerToClient(MOCK_SERVER_CRAWLER_DATA); + +describe('CrawlerLogic', () => { + const { mount, unmount } = new LogicMounter(CrawlerLogic); + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); // this should be run before every test to reset these mocks + mount(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('has expected default values', () => { + expect(CrawlerLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('clearTimeoutId', () => { + it('clears the timeout in the logic', () => { + mount({ + timeoutId: setTimeout(() => {}, 1), + }); + + CrawlerLogic.actions.clearTimeoutId(); + + expect(CrawlerLogic.values.timeoutId).toEqual(null); + }); + }); + + describe('onCreateNewTimeout', () => { + it('sets the timeout in the logic', () => { + const timeout = setTimeout(() => {}, 1); + + CrawlerLogic.actions.onCreateNewTimeout(timeout); + + expect(CrawlerLogic.values.timeoutId).toEqual(timeout); + }); + }); + + describe('onReceiveCrawlerData', () => { + const crawlerData: CrawlerData = { + domains: [ + { + id: '507f1f77bcf86cd799439011', + createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', + url: 'elastic.co', + documentCount: 13, + sitemaps: [], + entryPoints: [], + crawlRules: [], + defaultCrawlRule: DEFAULT_CRAWL_RULE, + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], + }, + ], + }; + + beforeEach(() => { + CrawlerLogic.actions.onReceiveCrawlerData(crawlerData); + }); + + it('should set all received data as top-level values', () => { + expect(CrawlerLogic.values.domains).toEqual(crawlerData.domains); + }); + + it('should set dataLoading to false', () => { + expect(CrawlerLogic.values.dataLoading).toEqual(false); + }); + }); + + describe('onReceiveCrawlRequests', () => { + const crawlRequests: CrawlRequest[] = [ + { + id: '618d0e66abe97bc688328900', + status: CrawlerStatus.Pending, + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + ]; + + beforeEach(() => { + CrawlerLogic.actions.onReceiveCrawlRequests(crawlRequests); + }); + + it('should set the crawl requests', () => { + expect(CrawlerLogic.values.crawlRequests).toEqual(crawlRequests); + }); + }); + }); + + describe('listeners', () => { + describe('fetchCrawlerData', () => { + it('updates logic with data that has been converted from server to client', async () => { + jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); + http.get.mockReturnValueOnce(Promise.resolve(MOCK_SERVER_CRAWLER_DATA)); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/crawler'); + expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( + MOCK_CLIENT_CRAWLER_DATA + ); + }); + + it('calls flashApiErrors when there is an error on the request for crawler data', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('startCrawl', () => { + describe('success path', () => { + it('creates a new crawl request and then fetches the latest crawl requests', async () => { + jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + http.post.mockReturnValueOnce(Promise.resolve()); + + CrawlerLogic.actions.startCrawl(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/crawler/crawl_requests' + ); + expect(CrawlerLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); + }); + }); + + describe('on failure', () => { + it('flashes an error message', async () => { + http.post.mockReturnValueOnce(Promise.reject('error')); + + CrawlerLogic.actions.startCrawl(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); + + describe('stopCrawl', () => { + describe('success path', () => { + it('stops the crawl starts and then fetches the latest crawl requests', async () => { + jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + http.post.mockReturnValueOnce(Promise.resolve()); + + CrawlerLogic.actions.stopCrawl(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/crawler/crawl_requests/cancel' + ); + expect(CrawlerLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); + }); + }); + + describe('on failure', () => { + it('flashes an error message', async () => { + jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + http.post.mockReturnValueOnce(Promise.reject('error')); + + CrawlerLogic.actions.stopCrawl(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); + + describe('createNewTimeoutForCrawlRequests', () => { + it('saves the timeout ID in the logic', () => { + jest.spyOn(CrawlerLogic.actions, 'onCreateNewTimeout'); + jest.spyOn(CrawlerLogic.actions, 'getLatestCrawlRequests'); + + CrawlerLogic.actions.createNewTimeoutForCrawlRequests(2000); + + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 2000); + expect(CrawlerLogic.actions.onCreateNewTimeout).toHaveBeenCalled(); + + jest.runAllTimers(); + + expect(CrawlerLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); + }); + + it('clears a timeout if one already exists', () => { + const timeoutId = setTimeout(() => {}, 1); + mount({ + timeoutId, + }); + + CrawlerLogic.actions.createNewTimeoutForCrawlRequests(2000); + + expect(clearTimeout).toHaveBeenCalledWith(timeoutId); + }); + }); + + describe('getLatestCrawlRequests', () => { + describe('on success', () => { + [ + CrawlerStatus.Pending, + CrawlerStatus.Starting, + CrawlerStatus.Running, + CrawlerStatus.Canceling, + ].forEach((status) => { + it(`creates a new timeout for status ${status}`, async () => { + jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlRequests'); + http.get.mockReturnValueOnce(Promise.resolve([{ status }])); + + CrawlerLogic.actions.getLatestCrawlRequests(); + await nextTick(); + + expect(CrawlerLogic.actions.createNewTimeoutForCrawlRequests).toHaveBeenCalled(); + }); + }); + + [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].forEach((status) => { + it(`clears the timeout and fetches data for status ${status}`, async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + http.get.mockReturnValueOnce(Promise.resolve([{ status }])); + + CrawlerLogic.actions.getLatestCrawlRequests(); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); + }); + + it(`optionally supresses fetching data for status ${status}`, async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + http.get.mockReturnValueOnce(Promise.resolve([{ status }])); + + CrawlerLogic.actions.getLatestCrawlRequests(false); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalledTimes(0); + }); + }); + }); + + describe('on failure', () => { + it('creates a new timeout', async () => { + jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlRequests'); + http.get.mockReturnValueOnce(Promise.reject()); + + CrawlerLogic.actions.getLatestCrawlRequests(); + await nextTick(); + + expect(CrawlerLogic.actions.createNewTimeoutForCrawlRequests).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('selectors', () => { + describe('mostRecentCrawlRequestStatus', () => { + it('is Success when there are no crawl requests', () => { + mount({ + crawlRequests: [], + }); + + expect(CrawlerLogic.values.mostRecentCrawlRequestStatus).toEqual(CrawlerStatus.Success); + }); + + it('is Success when there are only crawl requests', () => { + mount({ + crawlRequests: [ + { + id: '2', + status: CrawlerStatus.Skipped, + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + { + id: '1', + status: CrawlerStatus.Skipped, + createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + ], + }); + + expect(CrawlerLogic.values.mostRecentCrawlRequestStatus).toEqual(CrawlerStatus.Success); + }); + + it('is the first non-skipped crawl request status', () => { + mount({ + crawlRequests: [ + { + id: '3', + status: CrawlerStatus.Skipped, + createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + { + id: '2', + status: CrawlerStatus.Failed, + createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + { + id: '1', + status: CrawlerStatus.Success, + createdAt: 'Mon, 29 Aug 2020 17:00:00 +0000', + beganAt: null, + completedAt: null, + }, + ], + }); + + expect(CrawlerLogic.values.mostRecentCrawlRequestStatus).toEqual(CrawlerStatus.Failed); + }); + }); + }); + + describe('events', () => { + describe('beforeUnmount', () => { + it('clears the timeout if there is one', () => { + jest.spyOn(global, 'setTimeout'); + + mount({ + timeoutId: setTimeout(() => {}, 1), + }); + unmount(); + + expect(setTimeout).toHaveBeenCalled(); + }); + + it('does not crash if no timeout exists', () => { + mount({ timeoutId: null }); + unmount(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts new file mode 100644 index 0000000000000..b9782d84a74cc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors } from '../../../shared/flash_messages'; + +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; + +import { + CrawlerData, + CrawlerDomain, + CrawlRequest, + CrawlRequestFromServer, + CrawlerStatus, +} from './types'; +import { crawlerDataServerToClient, crawlRequestServerToClient } from './utils'; + +const POLLING_DURATION = 1000; +const POLLING_DURATION_ON_FAILURE = 5000; + +export interface CrawlerValues { + crawlRequests: CrawlRequest[]; + dataLoading: boolean; + domains: CrawlerDomain[]; + mostRecentCrawlRequestStatus: CrawlerStatus; + timeoutId: NodeJS.Timeout | null; +} + +interface CrawlerActions { + clearTimeoutId(): void; + createNewTimeoutForCrawlRequests(duration: number): { duration: number }; + fetchCrawlerData(): void; + getLatestCrawlRequests(refreshData?: boolean): { refreshData?: boolean }; + onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; + onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; + onReceiveCrawlRequests(crawlRequests: CrawlRequest[]): { crawlRequests: CrawlRequest[] }; + startCrawl(): void; + stopCrawl(): void; +} + +export const CrawlerLogic = kea>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'], + actions: { + clearTimeoutId: true, + createNewTimeoutForCrawlRequests: (duration) => ({ duration }), + fetchCrawlerData: true, + getLatestCrawlRequests: (refreshData) => ({ refreshData }), + onCreateNewTimeout: (timeoutId) => ({ timeoutId }), + onReceiveCrawlerData: (data) => ({ data }), + onReceiveCrawlRequests: (crawlRequests) => ({ crawlRequests }), + startCrawl: () => null, + stopCrawl: () => null, + }, + reducers: { + dataLoading: [ + true, + { + onReceiveCrawlerData: () => false, + }, + ], + domains: [ + [], + { + onReceiveCrawlerData: (_, { data: { domains } }) => domains, + }, + ], + crawlRequests: [ + [], + { + onReceiveCrawlRequests: (_, { crawlRequests }) => crawlRequests, + }, + ], + timeoutId: [ + null, + { + clearTimeoutId: () => null, + onCreateNewTimeout: (_, { timeoutId }) => timeoutId, + }, + ], + }, + selectors: ({ selectors }) => ({ + mostRecentCrawlRequestStatus: [ + () => [selectors.crawlRequests], + (crawlRequests: CrawlerValues['crawlRequests']) => { + const eligibleCrawlRequests = crawlRequests.filter( + (req) => req.status !== CrawlerStatus.Skipped + ); + if (eligibleCrawlRequests.length === 0) { + return CrawlerStatus.Success; + } + return eligibleCrawlRequests[0].status; + }, + ], + }), + listeners: ({ actions, values }) => ({ + fetchCrawlerData: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.get(`/api/app_search/engines/${engineName}/crawler`); + + const crawlerData = crawlerDataServerToClient(response); + + actions.onReceiveCrawlerData(crawlerData); + } catch (e) { + flashAPIErrors(e); + } + }, + startCrawl: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + await http.post(`/api/app_search/engines/${engineName}/crawler/crawl_requests`); + actions.getLatestCrawlRequests(); + } catch (e) { + flashAPIErrors(e); + } + }, + stopCrawl: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + await http.post(`/api/app_search/engines/${engineName}/crawler/crawl_requests/cancel`); + actions.getLatestCrawlRequests(); + } catch (e) { + flashAPIErrors(e); + } + }, + createNewTimeoutForCrawlRequests: ({ duration }) => { + if (values.timeoutId) { + clearTimeout(values.timeoutId); + } + + const timeoutIdId = setTimeout(() => { + actions.getLatestCrawlRequests(); + }, duration); + + actions.onCreateNewTimeout(timeoutIdId); + }, + getLatestCrawlRequests: async ({ refreshData = true }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const crawlRequestsFromServer: CrawlRequestFromServer[] = await http.get( + `/api/app_search/engines/${engineName}/crawler/crawl_requests` + ); + const crawlRequests = crawlRequestsFromServer.map(crawlRequestServerToClient); + actions.onReceiveCrawlRequests(crawlRequests); + if ( + [ + CrawlerStatus.Pending, + CrawlerStatus.Starting, + CrawlerStatus.Running, + CrawlerStatus.Canceling, + ].includes(crawlRequests[0]?.status) + ) { + actions.createNewTimeoutForCrawlRequests(POLLING_DURATION); + } else if ( + [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].includes( + crawlRequests[0]?.status + ) + ) { + actions.clearTimeoutId(); + if (refreshData) { + actions.fetchCrawlerData(); + } + } + } catch (e) { + actions.createNewTimeoutForCrawlRequests(POLLING_DURATION_ON_FAILURE); + } + }, + }), + events: ({ values }) => ({ + beforeUnmount: () => { + if (values.timeoutId) { + clearTimeout(values.timeoutId); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 97c7a3e47ae59..705dfc44baa88 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; -import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues } from '../../../__mocks__/kea_logic'; import '../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -84,11 +83,6 @@ const crawlRequests: CrawlRequestFromServer[] = [ ]; describe('CrawlerOverview', () => { - const mockActions = { - fetchCrawlerData: jest.fn(), - getLatestCrawlRequests: jest.fn(), - }; - const mockValues = { dataLoading: false, domains, @@ -97,32 +91,27 @@ describe('CrawlerOverview', () => { beforeEach(() => { jest.clearAllMocks(); - setMockActions(mockActions); - }); - - it('calls fetchCrawlerData and starts polling on page load', () => { - setMockValues(mockValues); - - shallow(); - - expect(mockActions.fetchCrawlerData).toHaveBeenCalledTimes(1); - expect(mockActions.getLatestCrawlRequests).toHaveBeenCalledWith(false); }); it('contains a crawler status banner', () => { setMockValues(mockValues); + const wrapper = shallow(); expect(wrapper.find(CrawlerStatusBanner)).toHaveLength(1); }); it('contains a crawler status indicator', () => { + setMockValues(mockValues); + const wrapper = shallow(); expect(getPageHeaderActions(wrapper).find(CrawlerStatusIndicator)).toHaveLength(1); }); it('contains a popover to manage crawls', () => { + setMockValues(mockValues); + const wrapper = shallow(); expect(getPageHeaderActions(wrapper).find(ManageCrawlsPopover)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index e268acbae5c90..78e093f4199da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; -import { useActions, useValues } from 'kea'; +import { useValues } from 'kea'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; @@ -26,17 +26,10 @@ import { CrawlerStatusIndicator } from './components/crawler_status_indicator/cr import { DomainsTable } from './components/domains_table'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; import { CRAWLER_TITLE } from './constants'; -import { CrawlerOverviewLogic } from './crawler_overview_logic'; +import { CrawlerLogic } from './crawler_logic'; export const CrawlerOverview: React.FC = () => { - const { crawlRequests, dataLoading, domains } = useValues(CrawlerOverviewLogic); - - const { fetchCrawlerData, getLatestCrawlRequests } = useActions(CrawlerOverviewLogic); - - useEffect(() => { - fetchCrawlerData(); - getLatestCrawlRequests(false); - }, []); + const { crawlRequests, dataLoading, domains } = useValues(CrawlerLogic); return ( ({ + CrawlerLogic: { + actions: { + onReceiveCrawlerData: jest.fn(), + }, + }, +})); -import { CrawlerOverviewLogic, CrawlerOverviewValues } from './crawler_overview_logic'; -import { - CrawlerData, - CrawlerDataFromServer, - CrawlerDomain, - CrawlerPolicies, - CrawlerRules, - CrawlerStatus, - CrawlRequest, - CrawlRule, -} from './types'; -import { crawlerDataServerToClient } from './utils'; +import { nextTick } from '@kbn/test/jest'; -const DEFAULT_VALUES: CrawlerOverviewValues = { - crawlRequests: [], - dataLoading: true, - domains: [], - mostRecentCrawlRequestStatus: CrawlerStatus.Success, - timeoutId: null, -}; +import { CrawlerLogic } from './crawler_logic'; +import { CrawlerOverviewLogic } from './crawler_overview_logic'; -const DEFAULT_CRAWL_RULE: CrawlRule = { - id: '-', - policy: CrawlerPolicies.allow, - rule: CrawlerRules.regex, - pattern: '.*', -}; +import { CrawlerDataFromServer, CrawlerDomain } from './types'; +import { crawlerDataServerToClient } from './utils'; const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { domains: [ @@ -62,128 +48,19 @@ const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { const MOCK_CLIENT_CRAWLER_DATA = crawlerDataServerToClient(MOCK_SERVER_CRAWLER_DATA); describe('CrawlerOverviewLogic', () => { - const { mount, unmount } = new LogicMounter(CrawlerOverviewLogic); + const { mount } = new LogicMounter(CrawlerOverviewLogic); const { http } = mockHttpValues; const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); - jest.useFakeTimers(); // this should be run before every test to reset these mocks mount(); }); - afterAll(() => { - jest.useRealTimers(); - }); - - it('has expected default values', () => { - expect(CrawlerOverviewLogic.values).toEqual(DEFAULT_VALUES); - }); - - describe('actions', () => { - describe('clearTimeoutId', () => { - it('clears the timeout in the logic', () => { - mount({ - timeoutId: setTimeout(() => {}, 1), - }); - - CrawlerOverviewLogic.actions.clearTimeoutId(); - - expect(CrawlerOverviewLogic.values.timeoutId).toEqual(null); - }); - }); - - describe('onCreateNewTimeout', () => { - it('sets the timeout in the logic', () => { - const timeout = setTimeout(() => {}, 1); - - CrawlerOverviewLogic.actions.onCreateNewTimeout(timeout); - - expect(CrawlerOverviewLogic.values.timeoutId).toEqual(timeout); - }); - }); - - describe('onReceiveCrawlerData', () => { - const crawlerData: CrawlerData = { - domains: [ - { - id: '507f1f77bcf86cd799439011', - createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', - url: 'elastic.co', - documentCount: 13, - sitemaps: [], - entryPoints: [], - crawlRules: [], - defaultCrawlRule: DEFAULT_CRAWL_RULE, - deduplicationEnabled: false, - deduplicationFields: ['title'], - availableDeduplicationFields: ['title', 'description'], - }, - ], - }; - - beforeEach(() => { - CrawlerOverviewLogic.actions.onReceiveCrawlerData(crawlerData); - }); - - it('should set all received data as top-level values', () => { - expect(CrawlerOverviewLogic.values.domains).toEqual(crawlerData.domains); - }); - - it('should set dataLoading to false', () => { - expect(CrawlerOverviewLogic.values.dataLoading).toEqual(false); - }); - }); - - describe('onReceiveCrawlRequests', () => { - const crawlRequests: CrawlRequest[] = [ - { - id: '618d0e66abe97bc688328900', - status: CrawlerStatus.Pending, - createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - ]; - - beforeEach(() => { - CrawlerOverviewLogic.actions.onReceiveCrawlRequests(crawlRequests); - }); - - it('should set the crawl requests', () => { - expect(CrawlerOverviewLogic.values.crawlRequests).toEqual(crawlRequests); - }); - }); - }); - describe('listeners', () => { - describe('fetchCrawlerData', () => { - it('updates logic with data that has been converted from server to client', async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData'); - http.get.mockReturnValueOnce(Promise.resolve(MOCK_SERVER_CRAWLER_DATA)); - - CrawlerOverviewLogic.actions.fetchCrawlerData(); - await nextTick(); - - expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/crawler'); - expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( - MOCK_CLIENT_CRAWLER_DATA - ); - }); - - it('calls flashApiErrors when there is an error on the request for crawler data', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); - - CrawlerOverviewLogic.actions.fetchCrawlerData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); - }); - describe('deleteDomain', () => { it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData'); + jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); http.delete.mockReturnValue(Promise.resolve(MOCK_SERVER_CRAWLER_DATA)); CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); @@ -195,7 +72,7 @@ describe('CrawlerOverviewLogic', () => { query: { respond_with: 'crawler_details' }, } ); - expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( + expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( MOCK_CLIENT_CRAWLER_DATA ); expect(flashSuccessToast).toHaveBeenCalled(); @@ -210,241 +87,5 @@ describe('CrawlerOverviewLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); - - describe('startCrawl', () => { - describe('success path', () => { - it('creates a new crawl request and then fetches the latest crawl requests', async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests'); - http.post.mockReturnValueOnce(Promise.resolve()); - - CrawlerOverviewLogic.actions.startCrawl(); - await nextTick(); - - expect(http.post).toHaveBeenCalledWith( - '/api/app_search/engines/some-engine/crawler/crawl_requests' - ); - expect(CrawlerOverviewLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); - }); - }); - - describe('on failure', () => { - it('flashes an error message', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); - - CrawlerOverviewLogic.actions.startCrawl(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); - }); - }); - - describe('stopCrawl', () => { - describe('success path', () => { - it('stops the crawl starts and then fetches the latest crawl requests', async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests'); - http.post.mockReturnValueOnce(Promise.resolve()); - - CrawlerOverviewLogic.actions.stopCrawl(); - await nextTick(); - - expect(http.post).toHaveBeenCalledWith( - '/api/app_search/engines/some-engine/crawler/crawl_requests/cancel' - ); - expect(CrawlerOverviewLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); - }); - }); - - describe('on failure', () => { - it('flashes an error message', async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests'); - http.post.mockReturnValueOnce(Promise.reject('error')); - - CrawlerOverviewLogic.actions.stopCrawl(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); - }); - }); - - describe('createNewTimeoutForCrawlRequests', () => { - it('saves the timeout ID in the logic', () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'onCreateNewTimeout'); - jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests'); - - CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests(2000); - - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 2000); - expect(CrawlerOverviewLogic.actions.onCreateNewTimeout).toHaveBeenCalled(); - - jest.runAllTimers(); - - expect(CrawlerOverviewLogic.actions.getLatestCrawlRequests).toHaveBeenCalled(); - }); - - it('clears a timeout if one already exists', () => { - const timeoutId = setTimeout(() => {}, 1); - mount({ - timeoutId, - }); - - CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests(2000); - - expect(clearTimeout).toHaveBeenCalledWith(timeoutId); - }); - }); - - describe('getLatestCrawlRequests', () => { - describe('on success', () => { - [ - CrawlerStatus.Pending, - CrawlerStatus.Starting, - CrawlerStatus.Running, - CrawlerStatus.Canceling, - ].forEach((status) => { - it(`creates a new timeout for status ${status}`, async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'createNewTimeoutForCrawlRequests'); - http.get.mockReturnValueOnce(Promise.resolve([{ status }])); - - CrawlerOverviewLogic.actions.getLatestCrawlRequests(); - await nextTick(); - - expect( - CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests - ).toHaveBeenCalled(); - }); - }); - - [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].forEach((status) => { - it(`clears the timeout and fetches data for status ${status}`, async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'clearTimeoutId'); - jest.spyOn(CrawlerOverviewLogic.actions, 'fetchCrawlerData'); - http.get.mockReturnValueOnce(Promise.resolve([{ status }])); - - CrawlerOverviewLogic.actions.getLatestCrawlRequests(); - await nextTick(); - - expect(CrawlerOverviewLogic.actions.clearTimeoutId).toHaveBeenCalled(); - expect(CrawlerOverviewLogic.actions.fetchCrawlerData).toHaveBeenCalled(); - }); - - it(`optionally supresses fetching data for status ${status}`, async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'clearTimeoutId'); - jest.spyOn(CrawlerOverviewLogic.actions, 'fetchCrawlerData'); - http.get.mockReturnValueOnce(Promise.resolve([{ status }])); - - CrawlerOverviewLogic.actions.getLatestCrawlRequests(false); - await nextTick(); - - expect(CrawlerOverviewLogic.actions.clearTimeoutId).toHaveBeenCalled(); - expect(CrawlerOverviewLogic.actions.fetchCrawlerData).toHaveBeenCalledTimes(0); - }); - }); - }); - - describe('on failure', () => { - it('creates a new timeout', async () => { - jest.spyOn(CrawlerOverviewLogic.actions, 'createNewTimeoutForCrawlRequests'); - http.get.mockReturnValueOnce(Promise.reject()); - - CrawlerOverviewLogic.actions.getLatestCrawlRequests(); - await nextTick(); - - expect(CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests).toHaveBeenCalled(); - }); - }); - }); - }); - - describe('selectors', () => { - describe('mostRecentCrawlRequestStatus', () => { - it('is Success when there are no crawl requests', () => { - mount({ - crawlRequests: [], - }); - - expect(CrawlerOverviewLogic.values.mostRecentCrawlRequestStatus).toEqual( - CrawlerStatus.Success - ); - }); - - it('is Success when there are only crawl requests', () => { - mount({ - crawlRequests: [ - { - id: '2', - status: CrawlerStatus.Skipped, - createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - { - id: '1', - status: CrawlerStatus.Skipped, - createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - ], - }); - - expect(CrawlerOverviewLogic.values.mostRecentCrawlRequestStatus).toEqual( - CrawlerStatus.Success - ); - }); - - it('is the first non-skipped crawl request status', () => { - mount({ - crawlRequests: [ - { - id: '3', - status: CrawlerStatus.Skipped, - createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - { - id: '2', - status: CrawlerStatus.Failed, - createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - { - id: '1', - status: CrawlerStatus.Success, - createdAt: 'Mon, 29 Aug 2020 17:00:00 +0000', - beganAt: null, - completedAt: null, - }, - ], - }); - - expect(CrawlerOverviewLogic.values.mostRecentCrawlRequestStatus).toEqual( - CrawlerStatus.Failed - ); - }); - }); - }); - - describe('events', () => { - describe('beforeUnmount', () => { - it('clears the timeout if there is one', () => { - jest.spyOn(global, 'setTimeout'); - - mount({ - timeoutId: setTimeout(() => {}, 1), - }); - unmount(); - - expect(setTimeout).toHaveBeenCalled(); - }); - - it('does not crash if no timeout exists', () => { - mount({ timeoutId: null }); - unmount(); - }); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts index 0506f4ba647f7..79e3326347d68 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts @@ -12,115 +12,20 @@ import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_message import { HttpLogic } from '../../../shared/http'; import { EngineLogic } from '../engine'; -import { - CrawlerData, - CrawlerDomain, - CrawlRequest, - CrawlRequestFromServer, - CrawlerStatus, -} from './types'; -import { - crawlerDataServerToClient, - crawlRequestServerToClient, - getDeleteDomainSuccessMessage, -} from './utils'; - -const POLLING_DURATION = 1000; -const POLLING_DURATION_ON_FAILURE = 5000; - -export interface CrawlerOverviewValues { - crawlRequests: CrawlRequest[]; - dataLoading: boolean; - domains: CrawlerDomain[]; - mostRecentCrawlRequestStatus: CrawlerStatus; - timeoutId: NodeJS.Timeout | null; -} +import { CrawlerLogic } from './crawler_logic'; +import { CrawlerDomain } from './types'; +import { crawlerDataServerToClient, getDeleteDomainSuccessMessage } from './utils'; interface CrawlerOverviewActions { - clearTimeoutId(): void; - createNewTimeoutForCrawlRequests(duration: number): { duration: number }; deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; - fetchCrawlerData(): void; - getLatestCrawlRequests(refreshData?: boolean): { refreshData?: boolean }; - onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; - onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; - onReceiveCrawlRequests(crawlRequests: CrawlRequest[]): { crawlRequests: CrawlRequest[] }; - startCrawl(): void; - stopCrawl(): void; } -export const CrawlerOverviewLogic = kea< - MakeLogicType ->({ +export const CrawlerOverviewLogic = kea>({ path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'], actions: { - clearTimeoutId: true, - createNewTimeoutForCrawlRequests: (duration) => ({ duration }), deleteDomain: (domain) => ({ domain }), - fetchCrawlerData: true, - getLatestCrawlRequests: (refreshData) => ({ refreshData }), - onCreateNewTimeout: (timeoutId) => ({ timeoutId }), - onReceiveCrawlerData: (data) => ({ data }), - onReceiveCrawlRequests: (crawlRequests) => ({ crawlRequests }), - startCrawl: () => null, - stopCrawl: () => null, - }, - reducers: { - dataLoading: [ - true, - { - onReceiveCrawlerData: () => false, - }, - ], - domains: [ - [], - { - onReceiveCrawlerData: (_, { data: { domains } }) => domains, - }, - ], - crawlRequests: [ - [], - { - onReceiveCrawlRequests: (_, { crawlRequests }) => crawlRequests, - }, - ], - timeoutId: [ - null, - { - clearTimeoutId: () => null, - onCreateNewTimeout: (_, { timeoutId }) => timeoutId, - }, - ], }, - selectors: ({ selectors }) => ({ - mostRecentCrawlRequestStatus: [ - () => [selectors.crawlRequests], - (crawlRequests: CrawlerOverviewValues['crawlRequests']) => { - const eligibleCrawlRequests = crawlRequests.filter( - (req) => req.status !== CrawlerStatus.Skipped - ); - if (eligibleCrawlRequests.length === 0) { - return CrawlerStatus.Success; - } - return eligibleCrawlRequests[0].status; - }, - ], - }), listeners: ({ actions, values }) => ({ - fetchCrawlerData: async () => { - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - - try { - const response = await http.get(`/api/app_search/engines/${engineName}/crawler`); - - const crawlerData = crawlerDataServerToClient(response); - - actions.onReceiveCrawlerData(crawlerData); - } catch (e) { - flashAPIErrors(e); - } - }, deleteDomain: async ({ domain }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; @@ -135,84 +40,11 @@ export const CrawlerOverviewLogic = kea< } ); const crawlerData = crawlerDataServerToClient(response); - actions.onReceiveCrawlerData(crawlerData); + CrawlerLogic.actions.onReceiveCrawlerData(crawlerData); flashSuccessToast(getDeleteDomainSuccessMessage(domain.url)); } catch (e) { flashAPIErrors(e); } }, - startCrawl: async () => { - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - - try { - await http.post(`/api/app_search/engines/${engineName}/crawler/crawl_requests`); - actions.getLatestCrawlRequests(); - } catch (e) { - flashAPIErrors(e); - } - }, - stopCrawl: async () => { - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - - try { - await http.post(`/api/app_search/engines/${engineName}/crawler/crawl_requests/cancel`); - actions.getLatestCrawlRequests(); - } catch (e) { - flashAPIErrors(e); - } - }, - createNewTimeoutForCrawlRequests: ({ duration }) => { - if (values.timeoutId) { - clearTimeout(values.timeoutId); - } - - const timeoutIdId = setTimeout(() => { - actions.getLatestCrawlRequests(); - }, duration); - - actions.onCreateNewTimeout(timeoutIdId); - }, - getLatestCrawlRequests: async ({ refreshData = true }) => { - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - - try { - const crawlRequestsFromServer: CrawlRequestFromServer[] = await http.get( - `/api/app_search/engines/${engineName}/crawler/crawl_requests` - ); - const crawlRequests = crawlRequestsFromServer.map(crawlRequestServerToClient); - actions.onReceiveCrawlRequests(crawlRequests); - if ( - [ - CrawlerStatus.Pending, - CrawlerStatus.Starting, - CrawlerStatus.Running, - CrawlerStatus.Canceling, - ].includes(crawlRequests[0]?.status) - ) { - actions.createNewTimeoutForCrawlRequests(POLLING_DURATION); - } else if ( - [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].includes( - crawlRequests[0]?.status - ) - ) { - actions.clearTimeoutId(); - if (refreshData) { - actions.fetchCrawlerData(); - } - } - } catch (e) { - actions.createNewTimeoutForCrawlRequests(POLLING_DURATION_ON_FAILURE); - } - }, - }), - events: ({ values }) => ({ - beforeUnmount: () => { - if (values.timeoutId) { - clearTimeout(values.timeoutId); - } - }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx index 20c377b67d191..8c49e97d6462b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx @@ -4,6 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { setMockActions } from '../../../__mocks__/kea_logic'; +import '../../../__mocks__/shallow_useeffect.mock'; +import '../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -14,14 +17,25 @@ import { CrawlerRouter } from './crawler_router'; import { CrawlerSingleDomain } from './crawler_single_domain'; describe('CrawlerRouter', () => { + const mockActions = { + fetchCrawlerData: jest.fn(), + getLatestCrawlRequests: jest.fn(), + }; + let wrapper: ShallowWrapper; beforeEach(() => { jest.clearAllMocks(); + setMockActions(mockActions); wrapper = shallow(); }); - it('renders a crawler single domain view', () => { + it('calls fetchCrawlerData and starts polling on page load', () => { + expect(mockActions.fetchCrawlerData).toHaveBeenCalledTimes(1); + expect(mockActions.getLatestCrawlRequests).toHaveBeenCalledWith(false); + }); + + it('renders a crawler views', () => { expect(wrapper.find(CrawlerOverview)).toHaveLength(1); expect(wrapper.find(CrawlerSingleDomain)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx index 436dcc4d3ea23..f95423cd2c704 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx @@ -5,15 +5,26 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; +import { useActions } from 'kea'; + import { ENGINE_CRAWLER_DOMAIN_PATH, ENGINE_CRAWLER_PATH } from '../../routes'; +import { CrawlerLogic } from './crawler_logic'; + import { CrawlerOverview } from './crawler_overview'; import { CrawlerSingleDomain } from './crawler_single_domain'; export const CrawlerRouter: React.FC = () => { + const { fetchCrawlerData, getLatestCrawlRequests } = useActions(CrawlerLogic); + + useEffect(() => { + fetchCrawlerData(); + getLatestCrawlRequests(false); + }, []); + return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 932af7a6ac93b..8cfbce6c10315 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -135,7 +135,7 @@ export interface CrawlerDomainValidationResultFromServer { }>; } -export type CrawlerDomainValidationStepState = '' | 'loading' | 'valid' | 'invalid'; +export type CrawlerDomainValidationStepState = '' | 'loading' | 'valid' | 'warning' | 'invalid'; export interface CrawlerDomainValidationStep { state: CrawlerDomainValidationStepState; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts index 1844932bac926..b679a7cc9c12c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts @@ -203,7 +203,7 @@ describe('crawlDomainValidationToResult', () => { expect(crawlDomainValidationToResult(data)).toEqual({ blockingFailure: false, - state: 'invalid', + state: 'warning', message: 'A warning, not failure', } as CrawlerDomainValidationStep); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts index 1f54db12a0217..e44e6c0e652fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts @@ -99,7 +99,7 @@ export function crawlDomainValidationToResult( if (warningResult) { return { - state: 'invalid', + state: 'warning', blockingFailure: !data.valid, message: warningResult.comment, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index 7ed9b9ea65025..90518de77f11f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -43,7 +43,7 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) =

.json, postCode: POST, @@ -106,17 +106,6 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) = )} description="" icon={} - betaBadgeLabel={i18n.translate( - 'xpack.enterpriseSearch.appSearch.documentCreation.buttons.betaTitle', - { defaultMessage: 'Beta' } - )} - betaBadgeTooltipContent={i18n.translate( - 'xpack.enterpriseSearch.appSearch.documentCreation.buttons.betaTooltip', - { - defaultMessage: - 'The Elastic Crawler is not GA. Please help us by reporting any bugs.', - } - )} to={crawlerLink} isDisabled={disabled} /> diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 6d99b6e2f5fe3..9a8f2267e7efe 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { mapValues, first, last, isNaN } from 'lodash'; +import { mapValues, first, last, isNaN, isNumber, isObject, has } from 'lodash'; import moment from 'moment'; import { ElasticsearchClient } from 'kibana/server'; import { @@ -23,7 +23,11 @@ import { UNGROUPED_FACTORY_KEY } from '../../common/utils'; import { MetricExpressionParams, Comparator, Aggregators } from '../types'; import { getElasticsearchMetricQuery } from './metric_query'; -interface Aggregation { +interface AggregationWithoutIntervals { + aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; +} + +interface AggregationWithIntervals { aggregatedIntervals: { buckets: Array<{ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; @@ -35,6 +39,14 @@ interface Aggregation { }; } +type Aggregation = AggregationWithIntervals | AggregationWithoutIntervals; + +function isAggregationWithIntervals( + subject: Aggregation | undefined +): subject is AggregationWithIntervals { + return isObject(subject) && has(subject, 'aggregatedIntervals'); +} + interface CompositeAggregationsResponse { groupings: { buckets: Aggregation[]; @@ -52,7 +64,7 @@ export const evaluateAlert = { const { criteria, groupBy, filterQuery, shouldDropPartialBuckets } = params; return Promise.all( @@ -105,7 +117,7 @@ const getMetric: ( timefield: string, groupBy: string | undefined | string[], filterQuery: string | undefined, - timeframe?: { start: number; end: number }, + timeframe?: { start?: number; end: number }, shouldDropPartialBuckets?: boolean ) => Promise> = async function ( esClient, @@ -124,10 +136,7 @@ const getMetric: ( const intervalAsSeconds = getIntervalInSeconds(interval); const intervalAsMS = intervalAsSeconds * 1000; - const to = moment(timeframe ? timeframe.end : Date.now()) - .add(1, timeUnit) - .startOf(timeUnit) - .valueOf(); + const to = moment(timeframe ? timeframe.end : Date.now()).valueOf(); // Rate aggregations need 5 buckets worth of data const minimumBuckets = aggType === Aggregators.RATE ? 5 : 1; @@ -135,7 +144,7 @@ const getMetric: ( const minimumFrom = to - intervalAsMS * minimumBuckets; const from = roundTimestamp( - timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, + timeframe && timeframe.start && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, timeUnit ); @@ -172,16 +181,26 @@ const getMetric: ( searchBody, bucketSelector, afterKeyHandler - )) as Array }>; - return compositeBuckets.reduce( + )) as Array; doc_count: number }>; + const groupedResults = compositeBuckets.reduce( (result, bucket) => ({ ...result, [Object.values(bucket.key) .map((value) => value) - .join(', ')]: getValuesFromAggregations(bucket, aggType, dropPartialBucketsOptions), + .join(', ')]: getValuesFromAggregations( + bucket, + aggType, + dropPartialBucketsOptions, + { + start: from, + end: to, + }, + bucket.doc_count + ), }), {} ); + return groupedResults; } const { body: result } = await esClient.search({ body: searchBody, @@ -192,7 +211,9 @@ const getMetric: ( [UNGROUPED_FACTORY_KEY]: getValuesFromAggregations( (result.aggregations! as unknown) as Aggregation, aggType, - dropPartialBucketsOptions + dropPartialBucketsOptions, + { start: from, end: to }, + isNumber(result.hits.total) ? result.hits.total : result.hits.total.value ), }; } catch (e) { @@ -221,7 +242,7 @@ interface DropPartialBucketOptions { const dropPartialBuckets = ({ from, to, bucketSizeInMillis }: DropPartialBucketOptions) => ( row: { key: string; - value: number; + value: number | null; } | null ) => { if (row == null) return null; @@ -230,20 +251,45 @@ const dropPartialBuckets = ({ from, to, bucketSizeInMillis }: DropPartialBucketO }; const getValuesFromAggregations = ( - aggregations: Aggregation, + aggregations: Aggregation | undefined, aggType: MetricExpressionParams['aggType'], - dropPartialBucketsOptions: DropPartialBucketOptions | null + dropPartialBucketsOptions: DropPartialBucketOptions | null, + timeFrame: { start: number; end: number }, + docCount?: number ) => { try { - const { buckets } = aggregations.aggregatedIntervals; + let buckets; + if (aggType === Aggregators.COUNT) { + buckets = [ + { + doc_count: docCount, + to_as_string: moment(timeFrame.end).toISOString(), + from_as_string: moment(timeFrame.start).toISOString(), + key_as_string: moment(timeFrame.start).toISOString(), + }, + ]; + } else if (isAggregationWithIntervals(aggregations)) { + buckets = aggregations.aggregatedIntervals.buckets; + } else { + buckets = [ + { + ...aggregations, + doc_count: docCount, + to_as_string: moment(timeFrame.end).toISOString(), + from_as_string: moment(timeFrame.start).toISOString(), + key_as_string: moment(timeFrame.start).toISOString(), + }, + ]; + } + if (!buckets.length) return null; // No Data state - let mappedBuckets; + let mappedBuckets: Array<{ key: string; value: number | null } | null>; if (aggType === Aggregators.COUNT) { mappedBuckets = buckets.map((bucket) => ({ key: bucket.from_as_string, - value: bucket.doc_count, + value: bucket.doc_count || null, })); } else if (aggType === Aggregators.P95 || aggType === Aggregators.P99) { mappedBuckets = buckets.map((bucket) => { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts index dd29437275243..2ba8365d6b4a9 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts @@ -64,30 +64,4 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { ); }); }); - - describe('when passed a timeframe of 1 hour', () => { - const testTimeframe = { - start: moment().subtract(1, 'hour').valueOf(), - end: moment().valueOf(), - }; - const searchBodyWithoutGroupBy = getElasticsearchMetricQuery( - expressionParams, - timefield, - testTimeframe - ); - const searchBodyWithGroupBy = getElasticsearchMetricQuery( - expressionParams, - timefield, - testTimeframe, - groupBy - ); - test("generates 1 hour's worth of buckets", () => { - // @ts-ignore - expect(searchBodyWithoutGroupBy.aggs.aggregatedIntervals.date_range.ranges.length).toBe(60); - expect( - // @ts-ignore - searchBodyWithGroupBy.aggs.groupings.aggs.aggregatedIntervals.date_range.ranges.length - ).toBe(60); - }); - }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 66e112640c357..f5ff4448ecb60 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -7,7 +7,6 @@ import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Aggregators } from '../types'; -import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds'; import { createPercentileAggregation } from './create_percentile_aggregation'; import { calculateDateHistogramOffset } from '../../../metrics/lib/calculate_date_histogram_offset'; @@ -34,13 +33,9 @@ export const getElasticsearchMetricQuery = ( throw new Error('Can only aggregate without a metric if using the document count aggregator'); } const interval = `${timeSize}${timeUnit}`; - const intervalAsSeconds = getIntervalInSeconds(interval); - const intervalAsMS = intervalAsSeconds * 1000; const to = timeframe.end; const from = timeframe.start; - const deliveryDelay = 60 * 1000; // INFO: This allows us to account for any delay ES has in indexing the most recent data. - const aggregations = aggType === Aggregators.COUNT ? {} @@ -72,21 +67,7 @@ export const getElasticsearchMetricQuery = ( aggregations, }, } - : { - aggregatedIntervals: { - date_range: { - field: timefield, - // Generate an array of buckets, starting at `from` and ending at `to` - // This is usually only necessary for alert previews or rate aggs. Most alert evaluations - // will generate only one bucket from this logic. - ranges: Array.from(Array(Math.floor((to - from) / intervalAsMS)), (_, i) => ({ - from: from + intervalAsMS * i - deliveryDelay, - to: from + intervalAsMS * (i + 1) - deliveryDelay, - })), - }, - aggregations, - }, - }; + : aggregations; const aggs = groupBy ? { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 18de1a2ad5c00..8eb19ad582057 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -515,7 +515,7 @@ services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: a } if (metric === 'test.metric.2') { return elasticsearchClientMock.createSuccessTransportRequestPromise( - mocks.alternateMetricResponse(from) + mocks.alternateMetricResponse() ); } else if (metric === 'test.metric.3') { return elasticsearchClientMock.createSuccessTransportRequestPromise( @@ -524,9 +524,7 @@ services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: a : mocks.emptyMetricResponse ); } - return elasticsearchClientMock.createSuccessTransportRequestPromise( - mocks.basicMetricResponse(from) - ); + return elasticsearchClientMock.createSuccessTransportRequestPromise(mocks.basicMetricResponse()); }); services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { if (sourceId === 'alternate') diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index 409a4329aa65c..b1173f2d611c8 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -103,20 +103,26 @@ const bucketsC = (from: number) => [ }, ]; -export const basicMetricResponse = (from: number) => ({ - aggregations: { - aggregatedIntervals: { - buckets: bucketsA(from), +export const basicMetricResponse = () => ({ + hits: { + total: { + value: 1, }, }, + aggregations: { + aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, + }, }); -export const alternateMetricResponse = (from: number) => ({ - aggregations: { - aggregatedIntervals: { - buckets: bucketsB(from), +export const alternateMetricResponse = () => ({ + hits: { + total: { + value: 1, }, }, + aggregations: { + aggregatedValue: { value: 3, values: [{ key: 99.0, value: 3 }] }, + }, }); export const emptyMetricResponse = { diff --git a/x-pack/plugins/licensing/public/index.ts b/x-pack/plugins/licensing/public/index.ts index 1b7ff40d0f14a..ec1c0aa1538cd 100644 --- a/x-pack/plugins/licensing/public/index.ts +++ b/x-pack/plugins/licensing/public/index.ts @@ -5,12 +5,20 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110902 -/* eslint-disable @kbn/eslint/no_export_all */ +import type { PluginInitializerContext } from 'src/core/public'; -import { PluginInitializerContext } from 'src/core/public'; import { LicensingPlugin } from './plugin'; -export * from '../common/types'; -export { LicensingPluginSetup, LicensingPluginStart } from './types'; +export type { + LicenseCheckState, + LicenseType, + LicenseStatus, + LicenseFeature, + PublicLicense, + PublicFeatures, + PublicLicenseJSON, + LicenseCheck, + ILicense, +} from '../common/types'; +export type { LicensingPluginSetup, LicensingPluginStart } from './types'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); diff --git a/x-pack/plugins/licensing/public/services/index.ts b/x-pack/plugins/licensing/public/services/index.ts index f7e125a253293..19c7b58dbb9c4 100644 --- a/x-pack/plugins/licensing/public/services/index.ts +++ b/x-pack/plugins/licensing/public/services/index.ts @@ -5,8 +5,5 @@ * 2.0. */ -export { - FeatureUsageService, - FeatureUsageServiceSetup, - FeatureUsageServiceStart, -} from './feature_usage_service'; +export { FeatureUsageService } from './feature_usage_service'; +export type { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './feature_usage_service'; diff --git a/x-pack/plugins/licensing/server/index.ts b/x-pack/plugins/licensing/server/index.ts index 64933d0820cf3..a95842308e4eb 100644 --- a/x-pack/plugins/licensing/server/index.ts +++ b/x-pack/plugins/licensing/server/index.ts @@ -5,16 +5,35 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110902 -/* eslint-disable @kbn/eslint/no_export_all */ - import { PluginInitializerContext } from 'src/core/server'; import { LicensingPlugin } from './plugin'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); -export * from '../common/types'; -export { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; -export * from './types'; +export type { + LicenseCheckState, + LicenseType, + LicenseStatus, + LicenseFeature, + PublicLicense, + PublicFeatures, + PublicLicenseJSON, + LicenseCheck, + ILicense, +} from '../common/types'; + +export { LICENSE_TYPE } from '../common/types'; + +export type { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; + +export type { + ElasticsearchError, + LicensingApiRequestHandlerContext, + LicensingPluginSetup, + LicensingPluginStart, +} from './types'; + export { config } from './licensing_config'; -export { CheckLicense, wrapRouteWithLicenseCheck } from './wrap_route_with_license_check'; + +export type { CheckLicense } from './wrap_route_with_license_check'; +export { wrapRouteWithLicenseCheck } from './wrap_route_with_license_check'; diff --git a/x-pack/plugins/licensing/server/services/index.ts b/x-pack/plugins/licensing/server/services/index.ts index f7e125a253293..19c7b58dbb9c4 100644 --- a/x-pack/plugins/licensing/server/services/index.ts +++ b/x-pack/plugins/licensing/server/services/index.ts @@ -5,8 +5,5 @@ * 2.0. */ -export { - FeatureUsageService, - FeatureUsageServiceSetup, - FeatureUsageServiceStart, -} from './feature_usage_service'; +export { FeatureUsageService } from './feature_usage_service'; +export type { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './feature_usage_service'; diff --git a/x-pack/plugins/licensing/tsconfig.json b/x-pack/plugins/licensing/tsconfig.json index d8855fcd65912..355d99fa461b8 100644 --- a/x-pack/plugins/licensing/tsconfig.json +++ b/x-pack/plugins/licensing/tsconfig.json @@ -4,7 +4,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "isolatedModules": true, }, "include": [ "public/**/*", diff --git a/x-pack/plugins/ml/.gitignore b/x-pack/plugins/ml/.gitignore index 708c5b199467b..e0f20bbc48bda 100644 --- a/x-pack/plugins/ml/.gitignore +++ b/x-pack/plugins/ml/.gitignore @@ -1 +1,2 @@ routes_doc +server/routes/apidoc_scripts/header.md diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 4dcaca573fc17..cdef5a9c20dae 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -101,7 +101,7 @@ export function jobAuditMessagesRoutes({ router, routeGuard }: RouteInitializati /** * @apiGroup JobAuditMessages * - * @api {put} /api/ml/job_audit_messages/clear_messages/{jobId} Index annotation + * @api {put} /api/ml/job_audit_messages/clear_messages Index annotation * @apiName ClearJobAuditMessages * @apiDescription Clear the job audit messages. * diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 7aaa9c78602a9..c87c202c0ec1f 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -78,7 +78,8 @@ export class HeadlessChromiumDriverFactory { { viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone?: string }, pLogger: LevelLogger ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { - return Rx.Observable.create(async (observer: InnerSubscriber) => { + // FIXME: 'create' is deprecated + return Rx.Observable.create(async (observer: InnerSubscriber) => { const logger = pLogger.clone(['browser-driver']); logger.info(`Creating browser page driver`); diff --git a/x-pack/plugins/reporting/server/config/ui_settings.test.ts b/x-pack/plugins/reporting/server/config/ui_settings.test.ts index dcd12e4c05f3f..6f40bb648ba93 100644 --- a/x-pack/plugins/reporting/server/config/ui_settings.test.ts +++ b/x-pack/plugins/reporting/server/config/ui_settings.test.ts @@ -49,23 +49,57 @@ test('throws validation error if provided with data over max size', () => { }); test('throws validation error if provided with non-image data', () => { - const invalidErrorMatcher = /try a different image/; - - expect(() => PdfLogoSchema.validate('')).toThrowError(invalidErrorMatcher); - expect(() => PdfLogoSchema.validate(true)).toThrow(invalidErrorMatcher); - expect(() => PdfLogoSchema.validate(false)).toThrow(invalidErrorMatcher); - expect(() => PdfLogoSchema.validate({})).toThrow(invalidErrorMatcher); - expect(() => PdfLogoSchema.validate([])).toThrow(invalidErrorMatcher); - expect(() => PdfLogoSchema.validate(0)).toThrow(invalidErrorMatcher); - expect(() => PdfLogoSchema.validate(0x00f)).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate('')).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: Sorry, that file will not work. Please try a different image file. + - [1]: expected value to equal [null]" + `); + expect(() => PdfLogoSchema.validate(true)).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: expected value of type [string] but got [boolean] + - [1]: expected value to equal [null]" + `); + expect(() => PdfLogoSchema.validate(false)).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: expected value of type [string] but got [boolean] + - [1]: expected value to equal [null]" + `); + expect(() => PdfLogoSchema.validate({})).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: expected value of type [string] but got [Object] + - [1]: expected value to equal [null]" + `); + expect(() => PdfLogoSchema.validate([])).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: expected value of type [string] but got [Array] + - [1]: expected value to equal [null]" + `); + expect(() => PdfLogoSchema.validate(0)).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: expected value of type [string] but got [number] + - [1]: expected value to equal [null]" + `); + expect(() => PdfLogoSchema.validate(0x00f)).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: expected value of type [string] but got [number] + - [1]: expected value to equal [null]" + `); const csvString = `data:text/csv;base64,Il9pZCIsIl9pbmRleCIsIl9zY29yZSIsIl90eXBlIiwiZm9vLmJhciIsImZvby5iYXIua2V5d29yZCIKZjY1QU9IZ0J5bFZmWW04W` + `TRvb1EsYmVlLDEsIi0iLGJheixiYXoKbks1QU9IZ0J5bFZmWW04WTdZcUcsYmVlLDEsIi0iLGJvbyxib28K`; - expect(() => PdfLogoSchema.validate(csvString)).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate(csvString)).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: Sorry, that file will not work. Please try a different image file. + - [1]: expected value to equal [null]" + `); const scriptString = `data:application/octet-stream;base64,QEVDSE8gT0ZGCldFRUtPRllSLkNPTSB8IEZJTkQgIlRoaXMgaXMiID4gVEVNUC5CQV` + `QKRUNITz5USElTLkJBVCBTRVQgV0VFSz0lJTMKQ0FMTCBURU1QLkJBVApERUwgIFRFTVAuQkFUCkRFTCAgVEhJUy5CQVQKRUNITyBXZWVrICVXRUVLJQo=`; - expect(() => PdfLogoSchema.validate(scriptString)).toThrow(invalidErrorMatcher); + expect(() => PdfLogoSchema.validate(scriptString)).toThrowErrorMatchingInlineSnapshot(` + "types that failed validation: + - [0]: Sorry, that file will not work. Please try a different image file. + - [1]: expected value to equal [null]" + `); }); diff --git a/x-pack/plugins/reporting/server/config/ui_settings.ts b/x-pack/plugins/reporting/server/config/ui_settings.ts index 587997bd1ecbd..bd13dcb8320ff 100644 --- a/x-pack/plugins/reporting/server/config/ui_settings.ts +++ b/x-pack/plugins/reporting/server/config/ui_settings.ts @@ -17,7 +17,7 @@ const maxLogoSizeInBase64 = kbToBase64Length(200); const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/; const imageTypes = ['image/svg+xml', 'image/jpeg', 'image/png', 'image/gif']; -const isImageData = (str: any): boolean => { +const isImageData = (str: string) => { const matches = str.match(dataurlRegex); if (!matches) { @@ -33,7 +33,7 @@ const isImageData = (str: any): boolean => { return true; }; -const validatePdfLogoBase64String = (str: any) => { +const validatePdfLogoBase64String = (str: string) => { if (typeof str !== 'string' || !isImageData(str)) { return i18n.translate('xpack.reporting.uiSettings.validate.customLogo.badFile', { defaultMessage: `Sorry, that file will not work. Please try a different image file.`, @@ -46,7 +46,9 @@ const validatePdfLogoBase64String = (str: any) => { } }; -export const PdfLogoSchema = schema.nullable(schema.any({ validate: validatePdfLogoBase64String })); +export const PdfLogoSchema = schema.nullable( + schema.string({ validate: validatePdfLogoBase64String }) +); export function registerUiSettings(core: CoreSetup) { core.uiSettings.register({ diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts index 4e5309bcff5b1..0fc4e497e16a7 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts @@ -101,7 +101,10 @@ export class PdfMaker { this._addContents(contents); } - addImage(image: Buffer, opts = { title: '', description: '' }) { + addImage( + image: Buffer, + opts: { title?: string; description?: string } = { title: '', description: '' } + ) { const size = this._layout.getPdfImageSize(); const img = { image: `data:image/png;base64,${image.toString('base64')}`, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index a7e492b882c20..74b013edc8cab 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -73,8 +73,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.startAddImage(); tracker.endAddImage(); pdfOutput.addImage(screenshot.data, { - title: screenshot.title, - description: screenshot.description, + title: screenshot.title ?? undefined, + description: screenshot.description ?? undefined, }); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index 8cc2d4b3037b3..9be95223a8864 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -84,8 +84,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.startAddImage(); tracker.endAddImage(); pdfOutput.addImage(screenshot.data, { - title: screenshot.title, - description: screenshot.description, + title: screenshot.title ?? undefined, + description: screenshot.description ?? undefined, }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts index 61d31153265f3..39163843c732f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts @@ -45,7 +45,7 @@ export const getElementPositionAndAttributes = async ( }, attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => { const attribute = attributes[key]; - (result as any)[key] = element.getAttribute(attribute); + result[key] = element.getAttribute(attribute); return result; }, {} as AttributesMap), }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts index a265a24855efe..edd346c9b8928 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts @@ -8,13 +8,10 @@ import { HeadlessChromiumDriver } from '../../browsers'; import { createMockBrowserDriverFactory, - createMockConfig, createMockConfigSchema, - createMockLayoutInstance, createMockLevelLogger, createMockReportingCore, } from '../../test_helpers'; -import { LayoutInstance } from '../layouts'; import { getScreenshots } from './get_screenshots'; describe('getScreenshots', () => { @@ -35,17 +32,12 @@ describe('getScreenshots', () => { }, ]; - let layout: LayoutInstance; let logger: ReturnType; let browser: jest.Mocked; beforeEach(async () => { - const schema = createMockConfigSchema(); - const config = createMockConfig(schema); - const captureConfig = config.get('capture'); - const core = await createMockReportingCore(schema); + const core = await createMockReportingCore(createMockConfigSchema()); - layout = createMockLayoutInstance(captureConfig); logger = createMockLevelLogger(); await createMockBrowserDriverFactory(core, logger, { @@ -71,7 +63,7 @@ describe('getScreenshots', () => { }); it('should return screenshots', async () => { - await expect(getScreenshots(browser, layout, elementsPositionAndAttributes, logger)).resolves + await expect(getScreenshots(browser, elementsPositionAndAttributes, logger)).resolves .toMatchInlineSnapshot(` Array [ Object { @@ -117,7 +109,7 @@ describe('getScreenshots', () => { }); it('should forward elements positions', async () => { - await getScreenshots(browser, layout, elementsPositionAndAttributes, logger); + await getScreenshots(browser, elementsPositionAndAttributes, logger); expect(browser.screenshot).toHaveBeenCalledTimes(2); expect(browser.screenshot).toHaveBeenNthCalledWith( @@ -134,7 +126,7 @@ describe('getScreenshots', () => { browser.screenshot.mockResolvedValue(Buffer.from('')); await expect( - getScreenshots(browser, layout, elementsPositionAndAttributes, logger) + getScreenshots(browser, elementsPositionAndAttributes, logger) ).rejects.toBeInstanceOf(Error); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts index 77c732b3336be..9b5f234b78363 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts @@ -8,12 +8,10 @@ import { i18n } from '@kbn/i18n'; import { LevelLogger, startTrace } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; -import { LayoutInstance } from '../layouts'; import { ElementsPositionAndAttribute, Screenshot } from './'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, - layout: LayoutInstance, elementsPositionAndAttributes: ElementsPositionAndAttribute[], logger: LevelLogger ): Promise => { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index e6769739ac75a..6615cbdc79c94 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -21,7 +21,7 @@ export interface ScreenshotObservableOpts { } export interface AttributesMap { - [key: string]: any; + [key: string]: string | null; } export interface ElementPosition { @@ -45,8 +45,8 @@ export interface ElementsPositionAndAttribute { export interface Screenshot { data: Buffer; - title: string; - description: string; + title: string | null; + description: string | null; } export interface ScreenshotResults { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index e833a0dfcaf60..aeb3de0b04e4d 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -123,7 +123,7 @@ export function getScreenshots$( const elements = data.elementsPositionAndAttributes ? data.elementsPositionAndAttributes : getDefaultElementPosition(layout.getViewport(1)); - const screenshots = await getScreenshots(driver, layout, elements, logger); + const screenshots = await getScreenshots(driver, elements, logger); const { timeRange, error: setupError } = data; return { timeRange, diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 1e29efd9cce0b..84566eb9c250c 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -47,8 +47,8 @@ interface TaskExecutor extends Pick jobExecutor: RunTaskFn; } -function isOutput(output: any): output is CompletedReportOutput { - return output?.size != null; +function isOutput(output: CompletedReportOutput | Error): output is CompletedReportOutput { + return (output as CompletedReportOutput).size != null; } function reportFromTask(task: ReportTaskParams) { diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index 6c5d548e77020..6d844f9637a0b 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -36,7 +36,7 @@ describe('POST /diagnose/screenshot', () => { toPromise: () => (resp instanceof Error ? Promise.reject(resp) : Promise.resolve(resp)), }), })); - (generatePngObservableFactory as any).mockResolvedValue(generateMock); + (generatePngObservableFactory as jest.Mock).mockResolvedValue(generateMock); }; const config = createMockConfigSchema({ queue: { timeout: 120000 } }); diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts similarity index 84% rename from x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts rename to x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts index efdb91d948536..52e6eb87e05cd 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { Writable } from 'stream'; import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; -import { ReportingCore } from '../'; -import { runTaskFnFactory } from '../export_types/csv_searchsource_immediate/execute_job'; -import { JobParamsDownloadCSV } from '../export_types/csv_searchsource_immediate/types'; -import { LevelLogger as Logger } from '../lib'; -import { TaskRunResult } from '../lib/tasks'; -import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; -import { HandlerErrorFunction } from './types'; +import { Writable } from 'stream'; +import { ReportingCore } from '../../'; +import { runTaskFnFactory } from '../../export_types/csv_searchsource_immediate/execute_job'; +import { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; +import { LevelLogger as Logger } from '../../lib'; +import { TaskRunResult } from '../../lib/tasks'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; +import { RequestHandler } from '../lib/request_handler'; const API_BASE_URL_V1 = '/api/reporting/v1'; const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; @@ -32,7 +32,6 @@ export type CsvFromSavedObjectRequest = KibanaRequest { + async (user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['csv_searchsource_immediate']); const runTaskFn = runTaskFnFactory(reporting, logger); + const requestHandler = new RequestHandler(reporting, user, context, req, res, logger); try { let buffer = Buffer.from(''); @@ -107,7 +107,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( }); } catch (err) { logger.error(err); - return handleError(res, err); + return requestHandler.handleError(err); } } ) diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts similarity index 81% rename from x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts rename to x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts index c519616cda5fb..cfcb7d6d2b05c 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts @@ -7,19 +7,16 @@ import { schema } from '@kbn/config-schema'; import rison from 'rison-node'; -import { ReportingCore } from '../'; -import { API_BASE_URL } from '../../common/constants'; -import { BaseParams } from '../types'; -import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; -import { HandlerErrorFunction, HandlerFunction } from './types'; +import { ReportingCore } from '../..'; +import { API_BASE_URL } from '../../../common/constants'; +import { LevelLogger } from '../../lib'; +import { BaseParams } from '../../types'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; +import { RequestHandler } from '../lib/request_handler'; const BASE_GENERATE = `${API_BASE_URL}/generate`; -export function registerGenerateFromJobParams( - reporting: ReportingCore, - handler: HandlerFunction, - handleError: HandlerErrorFunction -) { +export function registerJobGenerationRoutes(reporting: ReportingCore, logger: LevelLogger) { const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; @@ -62,7 +59,6 @@ export function registerGenerateFromJobParams( }); } - const { exportType } = req.params; let jobParams; try { @@ -80,10 +76,12 @@ export function registerGenerateFromJobParams( }); } + const requestHandler = new RequestHandler(reporting, user, context, req, res, logger); + try { - return await handler(user, exportType, jobParams, context, req, res); + return await requestHandler.handleGenerateRequest(req.params.exportType, jobParams); } catch (err) { - return handleError(res, err); + return requestHandler.handleError(err); } }) ); diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generate/generation_from_jobparams.test.ts similarity index 94% rename from x-pack/plugins/reporting/server/routes/generation.test.ts rename to x-pack/plugins/reporting/server/routes/generate/generation_from_jobparams.test.ts index df5a85d71f49f..dff52f1f67464 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/generation_from_jobparams.test.ts @@ -12,15 +12,15 @@ import { of } from 'rxjs'; import { ElasticsearchClient } from 'kibana/server'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; -import { ReportingCore } from '..'; -import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { createMockLevelLogger, createMockReportingCore } from '../test_helpers'; +import { ReportingCore } from '../..'; +import { ExportTypesRegistry } from '../../lib/export_types_registry'; +import { createMockLevelLogger, createMockReportingCore } from '../../test_helpers'; import { createMockConfigSchema, createMockPluginSetup, -} from '../test_helpers/create_mock_reportingplugin'; -import { registerJobGenerationRoutes } from './generation'; -import type { ReportingRequestHandlerContext } from '../types'; +} from '../../test_helpers/create_mock_reportingplugin'; +import type { ReportingRequestHandlerContext } from '../../types'; +import { registerJobGenerationRoutes } from './generate_from_jobparams'; type SetupServerReturn = UnwrapPromise>; diff --git a/x-pack/plugins/reporting/server/routes/generate/index.ts b/x-pack/plugins/reporting/server/routes/generate/index.ts new file mode 100644 index 0000000000000..0df9b4a725768 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/generate/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; // FIXME: should not need to register each immediate export type separately +export { registerJobGenerationRoutes } from './generate_from_jobparams'; +export { registerLegacy } from './legacy'; diff --git a/x-pack/plugins/reporting/server/routes/legacy.ts b/x-pack/plugins/reporting/server/routes/generate/legacy.ts similarity index 61% rename from x-pack/plugins/reporting/server/routes/legacy.ts rename to x-pack/plugins/reporting/server/routes/generate/legacy.ts index 79f1b7f17c2da..92f1784dc8eca 100644 --- a/x-pack/plugins/reporting/server/routes/legacy.ts +++ b/x-pack/plugins/reporting/server/routes/generate/legacy.ts @@ -6,21 +6,16 @@ */ import { schema } from '@kbn/config-schema'; -import querystring from 'querystring'; -import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; -import { API_BASE_URL } from '../../common/constants'; -import { HandlerErrorFunction, HandlerFunction } from './types'; -import { ReportingCore } from '../core'; -import { LevelLogger } from '../lib'; +import querystring, { ParsedUrlQueryInput } from 'querystring'; +import { API_BASE_URL } from '../../../common/constants'; +import { ReportingCore } from '../../core'; +import { LevelLogger } from '../../lib'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; +import { RequestHandler } from '../lib/request_handler'; const BASE_GENERATE = `${API_BASE_URL}/generate`; -export function registerLegacy( - reporting: ReportingCore, - handler: HandlerFunction, - handleError: HandlerErrorFunction, - logger: LevelLogger -) { +export function registerLegacy(reporting: ReportingCore, logger: LevelLogger) { const { router } = reporting.getPluginSetupDeps(); function createLegacyPdfRoute({ path, objectType }: { path: string; objectType: string }) { @@ -32,12 +27,15 @@ export function registerLegacy( validate: { params: schema.object({ savedObjectId: schema.string({ minLength: 3 }), + title: schema.string(), + browserTimezone: schema.string(), }), - query: schema.any(), + query: schema.maybe(schema.string()), }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { + const requestHandler = new RequestHandler(reporting, user, context, req, res, logger); const message = `The following URL is deprecated and will stop working in the next major version: ${req.url.pathname}${req.url.search}`; logger.warn(message, ['deprecation']); @@ -46,26 +44,19 @@ export function registerLegacy( title, savedObjectId, browserTimezone, - }: { title: string; savedObjectId: string; browserTimezone: string } = req.params as any; - const queryString = querystring.stringify(req.query as any); + }: { title: string; savedObjectId: string; browserTimezone: string } = req.params; + const queryString = querystring.stringify(req.query as ParsedUrlQueryInput | undefined); - return await handler( - user, - exportTypeId, - { - title, - objectType, - savedObjectId, - browserTimezone, - queryString, - version: reporting.getKibanaVersion(), - }, - context, - req, - res - ); + return await requestHandler.handleGenerateRequest(exportTypeId, { + title, + objectType, + savedObjectId, + browserTimezone, + queryString, + version: reporting.getKibanaVersion(), + }); } catch (err) { - throw handleError(res, err); + throw requestHandler.handleError(err); } }) ); diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts deleted file mode 100644 index adbfbda727af2..0000000000000 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ /dev/null @@ -1,92 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import { kibanaResponseFactory } from 'src/core/server'; -import { ReportingCore } from '../'; -import { API_BASE_URL } from '../../common/constants'; -import { LevelLogger as Logger } from '../lib'; -import { enqueueJob } from '../lib/enqueue_job'; -import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; -import { registerGenerateFromJobParams } from './generate_from_jobparams'; -import { registerLegacy } from './legacy'; -import { HandlerFunction } from './types'; - -const getDownloadBaseUrl = (reporting: ReportingCore) => { - const config = reporting.getConfig(); - return config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`; -}; - -export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { - /* - * Generates enqueued job details to use in responses - */ - const handler: HandlerFunction = async (user, exportTypeId, jobParams, context, req, res) => { - // ensure the async dependencies are loaded - if (!context.reporting) { - return res.custom({ statusCode: 503, body: 'Not Available' }); - } - - const licenseInfo = await reporting.getLicenseInfo(); - const licenseResults = licenseInfo[exportTypeId]; - - if (!licenseResults) { - return res.badRequest({ body: `Invalid export-type of ${exportTypeId}` }); - } - - if (!licenseResults.enableLinks) { - return res.forbidden({ body: licenseResults.message }); - } - - try { - const report = await enqueueJob( - reporting, - req, - context, - user, - exportTypeId, - jobParams, - logger - ); - - // return task manager's task information and the download URL - const downloadBaseUrl = getDownloadBaseUrl(reporting); - - return res.ok({ - headers: { - 'content-type': 'application/json', - }, - body: { - path: `${downloadBaseUrl}/${report._id}`, - job: report.toApiJSON(), - }, - }); - } catch (err) { - logger.error(err); - throw err; - } - }; - - /* - * Error should already have been logged by the time we get here - */ - function handleError(res: typeof kibanaResponseFactory, err: Error | Boom.Boom) { - if (err instanceof Boom.Boom) { - return res.customError({ - statusCode: err.output.statusCode, - body: err.output.payload.message, - }); - } - - // unknown error, can't convert to 4xx - throw err; - } - - registerGenerateFromJobParams(reporting, handler, handleError); - registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger); - registerLegacy(reporting, handler, handleError, logger); -} diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index a462da3849083..14a16e563ccbb 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -5,23 +5,22 @@ * 2.0. */ -import { LevelLogger as Logger } from '../lib'; +import { ReportingCore } from '..'; +import { LevelLogger } from '../lib'; import { registerDeprecationsRoutes } from './deprecations'; import { registerDiagnosticRoutes } from './diagnostic'; -import { registerJobGenerationRoutes } from './generation'; -import { registerJobInfoRoutes } from './jobs'; -import { ReportingCore } from '../core'; +import { + registerGenerateCsvFromSavedObjectImmediate, + registerJobGenerationRoutes, + registerLegacy, +} from './generate'; +import { registerJobInfoRoutes } from './management'; -export function registerRoutes(reporting: ReportingCore, logger: Logger) { +export function registerRoutes(reporting: ReportingCore, logger: LevelLogger) { registerDeprecationsRoutes(reporting, logger); registerDiagnosticRoutes(reporting, logger); + registerGenerateCsvFromSavedObjectImmediate(reporting, logger); registerJobGenerationRoutes(reporting, logger); + registerLegacy(reporting, logger); registerJobInfoRoutes(reporting); } - -export interface ReportingRequestPre { - management: { - jobTypes: string[]; - }; - user: string; -} 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 c083849686ff0..89e6fcf7df21d 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 @@ -24,7 +24,7 @@ interface Payload { statusCode: number; content: string | Stream | ErrorFromPayload; contentType: string | null; - headers: Record; + headers: Record; } type TaskRunResult = Required['output']; diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 5b63b2627f931..4033b317bef62 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -60,6 +60,7 @@ export async function downloadJobResponseHandler( } catch (err) { const { logger } = reporting.getPluginSetupDeps(); logger.error(err); + throw err; } } diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index e15fa01362e97..afa83ed331672 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -11,9 +11,10 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { i18n } from '@kbn/i18n'; import { UnwrapPromise } from '@kbn/utility-types'; import { ElasticsearchClient } from 'src/core/server'; +import { PromiseType } from 'utility-types'; import { ReportingCore } from '../../'; -import { statuses } from '../../lib/statuses'; import { ReportApiJSON, ReportSource } from '../../../common/types'; +import { statuses } from '../../lib/statuses'; import { Report } from '../../lib/store'; import { ReportingUser } from '../../types'; @@ -58,9 +59,9 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory return `${config.get('index')}-*`; } - async function execQuery any>( - callback: T - ): Promise> | undefined> { + async function execQuery< + T extends (client: ElasticsearchClient) => Promise> | undefined> + >(callback: T): Promise> | undefined> { try { const { asInternalUser: client } = await reportingCore.getEsClient(); diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts new file mode 100644 index 0000000000000..d730da4803fe9 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { ReportingCore } from '../..'; +import { + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../test_helpers'; +import { BaseParams, ReportingRequestHandlerContext, ReportingSetup } from '../../types'; +import { RequestHandler } from './request_handler'; + +jest.mock('../../lib/enqueue_job', () => ({ + enqueueJob: () => ({ + _id: 'id-of-this-test-report', + toApiJSON: () => JSON.stringify({ id: 'id-of-this-test-report' }), + }), +})); + +const getMockContext = () => + (({ + core: coreMock.createRequestHandlerContext(), + } as unknown) as ReportingRequestHandlerContext); + +const getMockRequest = () => + ({ + url: { port: '5601', search: '', pathname: '/foo' }, + route: { path: '/foo', options: {} }, + } as KibanaRequest); + +const getMockResponseFactory = () => + (({ + ...httpServerMock.createResponseFactory(), + forbidden: (obj: unknown) => obj, + unauthorized: (obj: unknown) => obj, + } as unknown) as KibanaResponseFactory); + +const mockLogger = createMockLevelLogger(); + +describe('Handle request to generate', () => { + let reportingCore: ReportingCore; + let mockContext: ReturnType; + let mockRequest: ReturnType; + let mockResponseFactory: ReturnType; + let requestHandler: RequestHandler; + + const mockJobParams = {} as BaseParams; + + beforeEach(async () => { + reportingCore = await createMockReportingCore(createMockConfigSchema({})); + mockRequest = getMockRequest(); + + mockResponseFactory = getMockResponseFactory(); + (mockResponseFactory.ok as jest.Mock) = jest.fn((args: unknown) => args); + (mockResponseFactory.forbidden as jest.Mock) = jest.fn((args: unknown) => args); + (mockResponseFactory.badRequest as jest.Mock) = jest.fn((args: unknown) => args); + + mockContext = getMockContext(); + mockContext.reporting = {} as ReportingSetup; + requestHandler = new RequestHandler( + reportingCore, + { username: 'testymcgee' }, + mockContext, + mockRequest, + mockResponseFactory, + mockLogger + ); + }); + + test('disallows invalid export type', async () => { + expect(await requestHandler.handleGenerateRequest('neanderthals', mockJobParams)) + .toMatchInlineSnapshot(` + Object { + "body": "Invalid export-type of neanderthals", + } + `); + }); + + test('disallows unsupporting license', async () => { + (reportingCore.getLicenseInfo as jest.Mock) = jest.fn(() => ({ + csv: { enableLinks: false, message: `seeing this means the license isn't supported` }, + })); + + expect(await requestHandler.handleGenerateRequest('csv', mockJobParams)).toMatchInlineSnapshot(` + Object { + "body": "seeing this means the license isn't supported", + } + `); + }); + + test('generates the download path', async () => { + expect(await requestHandler.handleGenerateRequest('csv', mockJobParams)).toMatchInlineSnapshot(` + Object { + "body": Object { + "job": "{\\"id\\":\\"id-of-this-test-report\\"}", + "path": "undefined/api/reporting/jobs/download/id-of-this-test-report", + }, + "headers": Object { + "content-type": "application/json", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts new file mode 100644 index 0000000000000..8637000f41d95 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { ReportingCore } from '../..'; +import { API_BASE_URL } from '../../../common/constants'; +import { JobParamsPDFLegacy } from '../../export_types/printable_pdf/types'; +import { LevelLogger } from '../../lib'; +import { enqueueJob } from '../../lib/enqueue_job'; +import { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; + +export const handleUnavailable = (res: KibanaResponseFactory) => { + return res.custom({ statusCode: 503, body: 'Not Available' }); +}; + +const getDownloadBaseUrl = (reporting: ReportingCore) => { + const config = reporting.getConfig(); + return config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`; +}; + +export class RequestHandler { + constructor( + private reporting: ReportingCore, + private user: ReportingUser, + private context: ReportingRequestHandlerContext, + private req: KibanaRequest, + private res: KibanaResponseFactory, + private logger: LevelLogger + ) {} + + public async handleGenerateRequest( + exportTypeId: string, + jobParams: BaseParams | JobParamsPDFLegacy + ) { + // ensure the async dependencies are loaded + if (!this.context.reporting) { + return handleUnavailable(this.res); + } + + const licenseInfo = await this.reporting.getLicenseInfo(); + const licenseResults = licenseInfo[exportTypeId]; + + if (!licenseResults) { + return this.res.badRequest({ body: `Invalid export-type of ${exportTypeId}` }); + } + + if (!licenseResults.enableLinks) { + return this.res.forbidden({ body: licenseResults.message }); + } + + try { + const report = await enqueueJob( + this.reporting, + this.req, + this.context, + this.user, + exportTypeId, + jobParams, + this.logger + ); + + // return task manager's task information and the download URL + const downloadBaseUrl = getDownloadBaseUrl(this.reporting); + + return this.res.ok({ + headers: { 'content-type': 'application/json' }, + body: { + path: `${downloadBaseUrl}/${report._id}`, + job: report.toApiJSON(), + }, + }); + } catch (err) { + this.logger.error(err); + throw err; + } + } + + /* + * This method does not log the error, as it assumes the error has already + * been caught and logged for stack trace context, and then rethrown + */ + public handleError(err: Error | Boom.Boom) { + if (err instanceof Boom.Boom) { + return this.res.customError({ + statusCode: err.output.statusCode, + body: err.output.payload.message, + }); + } + + // unknown error, can't convert to 4xx + throw err; + } +} diff --git a/x-pack/plugins/reporting/server/routes/management/index.ts b/x-pack/plugins/reporting/server/routes/management/index.ts new file mode 100644 index 0000000000000..0c31b2b0d6a0c --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/management/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { registerJobInfoRoutes } from './jobs'; diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/management/jobs.test.ts similarity index 98% rename from x-pack/plugins/reporting/server/routes/jobs.test.ts rename to x-pack/plugins/reporting/server/routes/management/jobs.test.ts index 883970bd45a74..c14976f616c7b 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/management/jobs.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -jest.mock('../lib/content_stream', () => ({ +jest.mock('../../lib/content_stream', () => ({ getContentStream: jest.fn(), })); @@ -16,15 +16,15 @@ import { of } from 'rxjs'; import { ElasticsearchClient } from 'kibana/server'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; -import { ReportingCore } from '..'; -import { ReportingInternalSetup } from '../core'; -import { ContentStream, ExportTypesRegistry, getContentStream } from '../lib'; +import { ReportingCore } from '../..'; +import { ReportingInternalSetup } from '../../core'; +import { ContentStream, ExportTypesRegistry, getContentStream } from '../../lib'; import { createMockConfigSchema, createMockPluginSetup, createMockReportingCore, -} from '../test_helpers'; -import { ExportTypeDefinition, ReportingRequestHandlerContext } from '../types'; +} from '../../test_helpers'; +import { ExportTypeDefinition, ReportingRequestHandlerContext } from '../../types'; import { registerJobInfoRoutes } from './jobs'; type SetupServerReturn = UnwrapPromise>; diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/management/jobs.ts similarity index 91% rename from x-pack/plugins/reporting/server/routes/jobs.ts rename to x-pack/plugins/reporting/server/routes/management/jobs.ts index 6086c1b9eb872..99c317453ca0f 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/management/jobs.ts @@ -5,21 +5,18 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import { ROUTE_TAG_CAN_REDIRECT } from '../../../security/server'; -import { ReportingCore } from '../'; -import { API_BASE_URL } from '../../common/constants'; -import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; -import { jobsQueryFactory } from './lib/jobs_query'; -import { deleteJobResponseHandler, downloadJobResponseHandler } from './lib/job_response_handler'; +import { schema } from '@kbn/config-schema'; +import { ReportingCore } from '../../'; +import { ROUTE_TAG_CAN_REDIRECT } from '../../../../security/server'; +import { API_BASE_URL } from '../../../common/constants'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; +import { jobsQueryFactory } from '../lib/jobs_query'; +import { deleteJobResponseHandler, downloadJobResponseHandler } from '../lib/job_response_handler'; +import { handleUnavailable } from '../lib/request_handler'; const MAIN_ENTRY = `${API_BASE_URL}/jobs`; -const handleUnavailable = (res: any) => { - return res.custom({ statusCode: 503, body: 'Not Available' }); -}; - export function registerJobInfoRoutes(reporting: ReportingCore) { const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts deleted file mode 100644 index 336605e6ff9b9..0000000000000 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ /dev/null @@ -1,35 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaRequest, KibanaResponseFactory } from 'src/core/server'; -import type { - BaseParams, - BaseParamsLegacyPDF, - BasePayload, - ReportingRequestHandlerContext, - ReportingUser, -} from '../types'; - -export type HandlerFunction = ( - user: ReportingUser, - exportType: string, - jobParams: BaseParams | BaseParamsLegacyPDF, - context: ReportingRequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory -) => any; - -export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any; - -export interface QueuedJobPayload { - error?: boolean; - source: { - job: { - payload: BasePayload; - }; - }; -} diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx index 3e3c91133eab6..ac49d8e90498d 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx @@ -11,7 +11,7 @@ import { useGlobalHeaderPortal } from '../../../../common/hooks/use_global_heade const StyledStickyWrapper = styled.div` position: sticky; - z-index: ${(props) => props.theme.eui.euiZLevel2}; + z-index: ${(props) => props.theme.eui.euiZHeaderBelowDataGrid}; // TOP location is declared in src/public/rendering/_base.scss to keep in line with Kibana Chrome `; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index b8b6b9766bdde..057d28b0112ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -321,7 +321,7 @@ const EventsViewerComponent: React.FC = ({ refetch={refetch} /> - {graphEventId && } + {graphEventId && } = ({ const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; const graphOverlay = useMemo( () => - graphEventId != null && graphEventId.length > 0 ? ( - - ) : null, + graphEventId != null && graphEventId.length > 0 ? : null, [graphEventId, id] ); const setQuery = useCallback( diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 0d4d52d338e56..2676343c24ac1 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -25,7 +25,7 @@ import { State } from '../../store'; import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; const TopNContainer = styled.div` - width: 600px; + min-width: 600px; `; const CloseButton = styled(EuiButtonIcon)` diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx index e19db8bb94b46..bd9c16e3d88e6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -28,7 +28,6 @@ export const resetScroll = () => { } }, 0); }; - interface GlobalFullScreen { globalFullScreen: boolean; setGlobalFullScreen: (fullScreen: boolean) => void; @@ -46,10 +45,10 @@ export const useGlobalFullScreen = (): GlobalFullScreen => { const setGlobalFullScreen = useCallback( (fullScreen: boolean) => { if (fullScreen) { - document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME); + document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody'); resetScroll(); } else { - document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME); + document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody'); resetScroll(); } @@ -71,9 +70,15 @@ export const useTimelineFullScreen = (): TimelineFullScreen => { const dispatch = useDispatch(); const timelineFullScreen = useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; - const setTimelineFullScreen = useCallback( - (fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })), + (fullScreen: boolean) => { + if (fullScreen) { + document.body.classList.add('euiDataGrid__restrictBody'); + } else { + document.body.classList.remove('euiDataGrid__restrictBody'); + } + dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })); + }, [dispatch] ); const memoizedReturn = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index fb772986bc679..cb6536d585c1e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -242,6 +242,9 @@ export const mockGlobalState: State = { activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.notes, deletedEventIds: [], + documentType: '', + queryFields: [], + selectAll: false, id: 'test', savedObjectId: null, columns: defaultHeaders, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 1c16fda54b90a..b601e531d4991 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -1979,6 +1979,7 @@ export const mockTimelineModel: TimelineModel = { }, deletedEventIds: [], description: 'This is a sample rule description', + documentType: '', eqlOptions: { eventCategoryField: 'event.category', tiebreakerField: 'event.sequence', @@ -2017,6 +2018,7 @@ export const mockTimelineModel: TimelineModel = { kqlQuery: { filterQuery: null, }, + queryFields: [], itemsPerPage: 25, itemsPerPageOptions: [10, 25, 50, 100], loadingEventIds: [], @@ -2024,6 +2026,7 @@ export const mockTimelineModel: TimelineModel = { pinnedEventIds: {}, pinnedEventsSaveObject: {}, savedObjectId: 'ef579e40-jibber-jabber', + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -2110,6 +2113,7 @@ export const defaultTimelineProps: CreateTimelineProps = { dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' }, deletedEventIds: [], description: '', + documentType: '', eqlOptions: { eventCategoryField: 'event.category', query: '', @@ -2141,7 +2145,9 @@ export const defaultTimelineProps: CreateTimelineProps = { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: null, + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 69160d90a011e..e7a8ba91cff8f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -148,6 +148,7 @@ describe('alert actions', () => { }, deletedEventIds: [], description: 'This is a sample rule description', + documentType: '', eqlOptions: { eventCategoryField: 'event.category', query: '', @@ -204,7 +205,9 @@ describe('alert actions', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: null, + selectAll: false, selectedEventIds: {}, show: true, showCheckboxes: false, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx index 2e23ecc648aee..21d0e132599fb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx @@ -13,6 +13,10 @@ import { setActiveTabTimeline, updateTimelineGraphEventId, } from '../../../../timelines/store/timeline/actions'; +import { + useGlobalFullScreen, + useTimelineFullScreen, +} from '../../../../common/containers/use_full_screen'; import { TimelineId, TimelineTabs } from '../../../../../common'; import { ACTION_INVESTIGATE_IN_RESOLVER } from '../../../../timelines/components/timeline/body/translations'; import { Ecs } from '../../../../../common/ecs'; @@ -35,13 +39,23 @@ export const useInvestigateInResolverContextItem = ({ }: InvestigateInResolverProps) => { const dispatch = useDispatch(); const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const { setGlobalFullScreen } = useGlobalFullScreen(); + const { setTimelineFullScreen } = useTimelineFullScreen(); const handleClick = useCallback(() => { + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })); if (timelineId === TimelineId.active) { + if (dataGridIsFullScreen) { + setTimelineFullScreen(true); + } dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); + } else { + if (dataGridIsFullScreen) { + setGlobalFullScreen(true); + } } onClose(); - }, [dispatch, ecsData._id, onClose, timelineId]); + }, [dispatch, ecsData._id, onClose, timelineId, setGlobalFullScreen, setTimelineFullScreen]); return isDisabled ? [] : [ diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index 1286208bff9e6..d672a3c699707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -44,12 +44,10 @@ describe('GraphOverlay', () => { }); describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { - const isEventViewer = true; - - test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => { + test('it has 100% width when NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -59,9 +57,9 @@ describe('GraphOverlay', () => { }); }); - test('it has a calculated width that makes room for the Timeline flyout button when isEventViewer is true in full screen mode', async () => { + test('it has a fixed position when in full screen mode', async () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ - globalFullScreen: true, // <-- true when an events viewer is in full screen mode + globalFullScreen: true, setGlobalFullScreen: jest.fn(), }); (useTimelineFullScreen as jest.Mock).mockReturnValue({ @@ -71,25 +69,24 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); await waitFor(() => { const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', 'calc(100% - 36px)'); + expect(overlayContainer).toHaveStyleRule('position', 'fixed'); }); }); }); describe('when used in the active timeline', () => { - const isEventViewer = false; const timelineId = TimelineId.active; - test('it has 100% width when isEventViewer is false and NOT in full screen mode', async () => { + test('it has 100% width when NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -99,7 +96,7 @@ describe('GraphOverlay', () => { }); }); - test('it has 100% width when isEventViewer is false and the active timeline is in full screen mode', async () => { + test('it has 100% width when the active timeline is in full screen mode', async () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: false, setGlobalFullScreen: jest.fn(), @@ -111,7 +108,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index a8cfea1de8e74..16459381a8431 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -41,13 +41,19 @@ import { import * as i18n from './translations'; const OverlayContainer = styled.div` - ${({ $restrictWidth }: { $restrictWidth: boolean }) => - ` - display: flex; - flex-direction: column; - flex: 1; - width: ${$restrictWidth ? 'calc(100% - 36px)' : '100%'}; - `} + display: flex; + flex-direction: column; + flex: 1; + width: 100%; +`; + +const FullScreenOverlayContainer = styled.div` + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: ${(props) => props.theme.eui.euiZLevel3}; `; const StyledResolver = styled(Resolver)` @@ -59,7 +65,6 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` `; interface OwnProps { - isEventViewer: boolean; timelineId: TimelineId; } @@ -111,18 +116,15 @@ NavigationComponent.displayName = 'NavigationComponent'; const Navigation = React.memo(NavigationComponent); -const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId }) => { +const GraphOverlayComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); - const onCloseOverlay = useCallback(() => { - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); - }, [dispatch, timelineId]); + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const graphEventId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId ); - - const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); - const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []); @@ -154,6 +156,16 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } [globalFullScreen, timelineId, timelineFullScreen] ); + const isInTimeline = timelineId === TimelineId.active; + const onCloseOverlay = useCallback(() => { + if (timelineId === TimelineId.active) { + setTimelineFullScreen(false); + } else { + setGlobalFullScreen(false); + } + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]); + const toggleFullScreen = useCallback(() => { if (timelineId === TimelineId.active) { setTimelineFullScreen(!timelineFullScreen); @@ -173,41 +185,71 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } [] ); const existingIndexNames = useDeepEqualSelector(existingIndexNamesSelector); - - return ( - - - - - + + + + + + + + {graphEventId !== undefined ? ( + - - - - {graphEventId !== undefined ? ( - - ) : ( - - + ) : ( + + + + )} + + ); + } else { + return ( + + + + + + - )} - - ); + + {graphEventId !== undefined ? ( + + ) : ( + + + + )} + + ); + } }; export const GraphOverlay = React.memo(GraphOverlayComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index ae15768d26e70..37bdfd38bf8bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -296,6 +296,7 @@ describe('helpers', () => { dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -328,7 +329,9 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: 'savedObject-1', + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -366,6 +369,7 @@ describe('helpers', () => { dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -398,7 +402,9 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: 'savedObject-1', + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -436,6 +442,7 @@ describe('helpers', () => { dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -468,7 +475,9 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: 'savedObject-1', + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -504,6 +513,7 @@ describe('helpers', () => { dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -536,7 +546,9 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: 'savedObject-1', + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -577,6 +589,7 @@ describe('helpers', () => { dataProviders: [], dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -612,6 +625,8 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -680,6 +695,7 @@ describe('helpers', () => { dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' }, dataProviders: [], description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -758,6 +774,8 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -791,6 +809,7 @@ describe('helpers', () => { dataProviders: [], dateRange: { end: '2020-10-28T11:37:31.655Z', start: '2020-10-27T11:37:31.655Z' }, description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -823,7 +842,9 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: 'savedObject-1', + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, @@ -861,6 +882,7 @@ describe('helpers', () => { dataProviders: [], dateRange: { end: '2020-07-08T08:20:18.966Z', start: '2020-07-07T08:20:18.966Z' }, description: '', + documentType: '', deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', @@ -893,7 +915,9 @@ describe('helpers', () => { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: 'savedObject-1', + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx index 1678a92c4cdaa..64d3f01cff265 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx @@ -26,7 +26,7 @@ const GraphTabContentComponent: React.FC = ({ timelineId } return null; } - return ; + return ; }; GraphTabContentComponent.displayName = 'GraphTabContentComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts index f1b5f6a944678..8fbb330d51231 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts @@ -28,6 +28,17 @@ const useKibanaMock = useKibana as jest.Mocked; const getExpectedColumns = (model: TimelineModel) => model.columns.map(migrateColumnWidthToInitialWidth).map(migrateColumnLabelToDisplayAsText); +const { + documentType, + filterManager, + isLoading, + loadingText, + queryFields, + selectAll, + unit, + ...timelineToStore +} = mockTimelineModel; + describe('SiemLocalStorage', () => { const { localStorage, storage } = createSecuritySolutionStorageMock(); @@ -41,7 +52,7 @@ describe('SiemLocalStorage', () => { const timelineStorage = useTimelinesStorage(); timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, }); }); @@ -50,8 +61,8 @@ describe('SiemLocalStorage', () => { timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, - [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, + [TimelineId.hostsPageExternalAlerts]: timelineToStore, }); }); }); @@ -63,8 +74,8 @@ describe('SiemLocalStorage', () => { timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); const timelines = timelineStorage.getAllTimelines(); expect(timelines).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, - [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, + [TimelineId.hostsPageExternalAlerts]: timelineToStore, }); }); @@ -80,7 +91,7 @@ describe('SiemLocalStorage', () => { const timelineStorage = useTimelinesStorage(); timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); const timeline = timelineStorage.getTimelineById(TimelineId.hostsPageEvents); - expect(timeline).toEqual(mockTimelineModel); + expect(timeline).toEqual(timelineToStore); }); }); @@ -94,8 +105,8 @@ describe('SiemLocalStorage', () => { TimelineId.hostsPageExternalAlerts, ]); expect(timelines).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, - [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, + [TimelineId.hostsPageExternalAlerts]: timelineToStore, }); }); @@ -126,7 +137,7 @@ describe('SiemLocalStorage', () => { TimelineId.hostsPageExternalAlerts, ]); expect(timelines).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, }); }); @@ -152,8 +163,8 @@ describe('SiemLocalStorage', () => { // all legacy `width` values are migrated to `initialWidth`: expect(timelines).toStrictEqual({ [TimelineId.hostsPageEvents]: { - ...mockTimelineModel, - columns: mockTimelineModel.columns.map((c) => ({ + ...timelineToStore, + columns: timelineToStore.columns.map((c) => ({ ...c, displayAsText: undefined, initialWidth: 98765, @@ -161,7 +172,7 @@ describe('SiemLocalStorage', () => { })), }, [TimelineId.hostsPageExternalAlerts]: { - ...mockTimelineModel, + ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, }); @@ -187,8 +198,8 @@ describe('SiemLocalStorage', () => { expect(timelines).toStrictEqual({ [TimelineId.hostsPageEvents]: { - ...mockTimelineModel, - columns: mockTimelineModel.columns.map((c) => ({ + ...timelineToStore, + columns: timelineToStore.columns.map((c) => ({ ...c, displayAsText: undefined, initialWidth: c.initialWidth, // initialWidth is unchanged @@ -196,7 +207,7 @@ describe('SiemLocalStorage', () => { })), }, [TimelineId.hostsPageExternalAlerts]: { - ...mockTimelineModel, + ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, }); @@ -223,15 +234,15 @@ describe('SiemLocalStorage', () => { // all legacy `label` values are migrated to `displayAsText`: expect(timelines).toStrictEqual({ [TimelineId.hostsPageEvents]: { - ...mockTimelineModel, - columns: mockTimelineModel.columns.map((c, i) => ({ + ...timelineToStore, + columns: timelineToStore.columns.map((c, i) => ({ ...c, displayAsText: `A legacy label ${i}`, label: `A legacy label ${i}`, })), }, [TimelineId.hostsPageExternalAlerts]: { - ...mockTimelineModel, + ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, }); @@ -259,8 +270,8 @@ describe('SiemLocalStorage', () => { expect(timelines).toStrictEqual({ [TimelineId.hostsPageEvents]: { - ...mockTimelineModel, - columns: mockTimelineModel.columns.map((c, i) => ({ + ...timelineToStore, + columns: timelineToStore.columns.map((c, i) => ({ ...c, displayAsText: 'Label will NOT be migrated to displayAsText, because displayAsText already has a value', @@ -268,7 +279,7 @@ describe('SiemLocalStorage', () => { })), }, [TimelineId.hostsPageExternalAlerts]: { - ...mockTimelineModel, + ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, }); @@ -293,11 +304,11 @@ describe('SiemLocalStorage', () => { expect(timelines).toStrictEqual({ [TimelineId.hostsPageEvents]: { - ...mockTimelineModel, + ...timelineToStore, columns: 'this is NOT an array', }, [TimelineId.hostsPageExternalAlerts]: { - ...mockTimelineModel, + ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, }); @@ -311,8 +322,8 @@ describe('SiemLocalStorage', () => { timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); const timelines = getAllTimelinesInStorage(storage); expect(timelines).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, - [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, + [TimelineId.hostsPageExternalAlerts]: timelineToStore, }); }); @@ -326,7 +337,7 @@ describe('SiemLocalStorage', () => { it('adds a timeline when storage is empty', () => { addTimelineInStorage(storage, TimelineId.hostsPageEvents, mockTimelineModel); expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, }); }); @@ -334,8 +345,8 @@ describe('SiemLocalStorage', () => { addTimelineInStorage(storage, TimelineId.hostsPageEvents, mockTimelineModel); addTimelineInStorage(storage, TimelineId.hostsPageExternalAlerts, mockTimelineModel); expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ - [TimelineId.hostsPageEvents]: mockTimelineModel, - [TimelineId.hostsPageExternalAlerts]: mockTimelineModel, + [TimelineId.hostsPageEvents]: timelineToStore, + [TimelineId.hostsPageExternalAlerts]: timelineToStore, }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index 99f45c7d9a4b4..dd60656933ba8 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -85,13 +85,29 @@ export const addTimelineInStorage = ( id: TimelineIdLiteral, timeline: TimelineModel ) => { + const timelineToStore = cleanStorageTimeline(timeline); const timelines = getAllTimelinesInStorage(storage); storage.set(LOCAL_STORAGE_TIMELINE_KEY, { ...timelines, - [id]: timeline, + [id]: timelineToStore, }); }; +const cleanStorageTimeline = (timeline: TimelineModel) => { + // discard unneeded fields to make sure the object serialization works + const { + documentType, + filterManager, + isLoading, + loadingText, + queryFields, + selectAll, + unit, + ...timelineToStore + } = timeline; + return timelineToStore; +}; + export const useTimelinesStorage = (): TimelinesStorage => { const { storage } = useKibana().services; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index f411c6ffac9b7..0ba3f91173d0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -19,6 +19,7 @@ export const timelineDefaults: SubsetTimelineModel & activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, columns: defaultHeaders, + documentType: '', defaultColumns: defaultHeaders, dataProviders: [], dateRange: { start, end }, @@ -51,6 +52,7 @@ export const timelineDefaults: SubsetTimelineModel & filterQuery: null, }, loadingEventIds: [], + queryFields: [], title: '', timelineType: TimelineType.default, templateTimelineId: null, @@ -59,6 +61,7 @@ export const timelineDefaults: SubsetTimelineModel & pinnedEventIds: {}, pinnedEventsSaveObject: {}, savedObjectId: null, + selectAll: false, selectedEventIds: {}, show: false, showCheckboxes: false, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index 8b40febbfe993..686c8220f677b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -92,6 +92,7 @@ describe('Epic Timeline', () => { ], deletedEventIds: [], description: '', + documentType: '', eqlOptions: { eventCategoryField: 'event.category', tiebreakerField: '', @@ -146,6 +147,8 @@ describe('Epic Timeline', () => { }, }, loadingEventIds: [], + queryFields: [], + selectAll: false, title: 'saved', timelineType: TimelineType.default, templateTimelineId: null, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index a2d7e2300d171..b53da997c08cb 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -85,10 +85,12 @@ export type SubsetTimelineModel = Readonly< | 'dataProviders' | 'deletedEventIds' | 'description' + | 'documentType' | 'eventType' | 'eventIdToNoteIds' | 'excludedRowRendererIds' | 'expandedDetail' + | 'footerText' | 'graphEventId' | 'highlightedDropAndProviderId' | 'historyIds' @@ -100,15 +102,18 @@ export type SubsetTimelineModel = Readonly< | 'itemsPerPageOptions' | 'kqlMode' | 'kqlQuery' + | 'queryFields' | 'title' | 'timelineType' | 'templateTimelineId' | 'templateTimelineVersion' | 'loadingEventIds' + | 'loadingText' | 'noteIds' | 'pinnedEventIds' | 'pinnedEventsSaveObject' | 'dateRange' + | 'selectAll' | 'selectedEventIds' | 'show' | 'showCheckboxes' @@ -116,6 +121,7 @@ export type SubsetTimelineModel = Readonly< | 'isSaving' | 'isLoading' | 'savedObjectId' + | 'unit' | 'version' | 'status' > diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 96ae11cb8afdc..c0dcba6920b60 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -88,6 +88,7 @@ const basicTimeline: TimelineModel = { }, deletedEventIds: [], description: '', + documentType: '', eqlOptions: { eventCategoryField: 'event.category', tiebreakerField: '', @@ -113,7 +114,9 @@ const basicTimeline: TimelineModel = { noteIds: [], pinnedEventIds: {}, pinnedEventsSaveObject: {}, + queryFields: [], savedObjectId: null, + selectAll: false, selectedEventIds: {}, show: true, showCheckboxes: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index a1f63a6d4e0c6..a4e812e8f111a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -121,10 +121,9 @@ export const buildSignalFromSequence = ( ): SignalHit => { const rule = buildRuleWithoutOverrides(ruleSO); const timestamp = new Date().toISOString(); - - const reason = buildReasonMessage({ rule }); - const signal: Signal = buildSignal(events, rule, reason); const mergedEvents = objectArrayIntersection(events.map((event) => event._source)); + const reason = buildReasonMessage({ rule, mergedDoc: mergedEvents as SignalSourceHit }); + const signal: Signal = buildSignal(events, rule, reason); return { ...mergedEvents, '@timestamp': timestamp, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts index 55cd7b942b3d8..49f70eafd7d3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts @@ -137,6 +137,52 @@ describe('get_filter', () => { }); }); + test('returns the query persisted to the threat_match rule, despite saved_id being specified', async () => { + const filter = await getFilter({ + type: 'threat_match', + filters: undefined, + language: 'kuery', + query: 'host.name: siem', + savedId: 'some-id', + services: servicesMock, + index: ['auditbeat-*'], + lists: [], + }); + expect(filter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'siem' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('returns the query persisted to the threshold rule, despite saved_id being specified', async () => { + const filter = await getFilter({ + type: 'threat_match', + filters: undefined, + language: 'kuery', + query: 'host.name: siem', + savedId: 'some-id', + services: servicesMock, + index: ['auditbeat-*'], + lists: [], + }); + expect(filter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'siem' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + test('throws on saved query if saved_id is undefined', async () => { await expect( getFilter({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 346c4adeba537..574020af45c15 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -94,9 +94,7 @@ export const getFilter = async ({ switch (type) { case 'threat_match': - case 'threshold': { - return savedId != null ? savedQueryFilter() : queryFilter(); - } + case 'threshold': case 'query': { return queryFilter(); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts index 1a383b51eb8d4..5b55df2bee936 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { buildCommonReasonMessage } from './reason_formatters'; +import { buildReasonMessageUtil } from './reason_formatters'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { SignalSourceHit } from './types'; @@ -14,26 +14,48 @@ describe('reason_formatter', () => { let mergedDoc: SignalSourceHit; beforeAll(() => { rule = { - name: 'What is in a name', + name: 'my-rule', risk_score: 9000, severity: 'medium', } as RulesSchema; // Cast here as all fields aren't required mergedDoc = { - _index: 'some-index', - _id: 'some-id', + _index: 'index-1', + _id: 'id-1', fields: { - 'host.name': ['party host'], - 'user.name': ['ferris bueller'], + 'destination.address': ['9.99.99.9'], + 'destination.port': ['6789'], + 'event.category': ['test'], + 'file.name': ['sample'], + 'host.name': ['host'], + 'process.name': ['doingThings.exe'], + 'process.parent.name': ['didThings.exe'], + 'source.address': ['1.11.11.1'], + 'source.port': ['1234'], + 'user.name': ['test-user'], '@timestamp': '2021-08-11T02:28:59.101Z', }, }; }); - describe('buildCommonReasonMessage', () => { + describe('buildReasonMessageUtil', () => { describe('when rule and mergedDoc are provided', () => { it('should return the full reason message', () => { - expect(buildCommonReasonMessage({ rule, mergedDoc })).toEqual( - 'Alert What is in a name created with a medium severity and risk score of 9000 by ferris bueller on party host.' + expect(buildReasonMessageUtil({ rule, mergedDoc })).toMatchInlineSnapshot( + `"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."` + ); + }); + }); + describe('when event category contains multiple items', () => { + it('should return the reason message with all categories showing', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'event.category': ['item one', 'item two'], + }, + }; + expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot( + `"item one, item two event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."` ); }); }); @@ -46,8 +68,8 @@ describe('reason_formatter', () => { 'host.name': ['-'], }, }; - expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc })).toEqual( - 'Alert What is in a name created with a medium severity and risk score of 9000 by ferris bueller.' + expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot( + `"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user created medium alert my-rule."` ); }); }); @@ -60,16 +82,102 @@ describe('reason_formatter', () => { 'user.name': ['-'], }, }; - expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc })).toEqual( - 'Alert What is in a name created with a medium severity and risk score of 9000 on party host.' + expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot( + `"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, on host created medium alert my-rule."` + ); + }); + }); + describe('when rule and mergedDoc are provided, but destination details are missing', () => { + it('should return the reason message without the destination port', () => { + const noDestinationPortDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'destination.port': ['-'], + }, + }; + expect( + buildReasonMessageUtil({ rule, mergedDoc: noDestinationPortDoc }) + ).toMatchInlineSnapshot( + `"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9 by test-user on host created medium alert my-rule."` + ); + }); + it('should return the reason message without destination details', () => { + const noDestinationPortDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'destination.address': ['-'], + 'destination.port': ['-'], + }, + }; + expect( + buildReasonMessageUtil({ rule, mergedDoc: noDestinationPortDoc }) + ).toMatchInlineSnapshot( + `"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, by test-user on host created medium alert my-rule."` + ); + }); + }); + describe('when rule and mergedDoc are provided, but source details are missing', () => { + it('should return the reason message without the source port', () => { + const noSourcePortDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'source.port': ['-'], + }, + }; + expect(buildReasonMessageUtil({ rule, mergedDoc: noSourcePortDoc })).toMatchInlineSnapshot( + `"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1 destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."` + ); + }); + it('should return the reason message without source details', () => { + const noSourcePortDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'source.address': ['-'], + 'source.port': ['-'], + }, + }; + expect(buildReasonMessageUtil({ rule, mergedDoc: noSourcePortDoc })).toMatchInlineSnapshot( + `"test event with process doingThings.exe, parent process didThings.exe, file sample, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."` + ); + }); + }); + describe('when rule and mergedDoc are provided, but process details missing', () => { + it('should return the reason message without process details', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'process.name': ['-'], + 'process.parent.name': ['-'], + }, + }; + expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot( + `"test event with file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."` + ); + }); + }); + describe('when rule and mergedDoc are provided without any fields of interest', () => { + it('should return the full reason message', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + 'event.category': ['test'], + 'user.name': ['test-user'], + '@timestamp': '2021-08-11T02:28:59.101Z', + }, + }; + expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot( + `"test event by test-user created medium alert my-rule."` ); }); }); describe('when only rule is provided', () => { it('should return the reason message without host name or user name', () => { - expect(buildCommonReasonMessage({ rule })).toEqual( - 'Alert What is in a name created with a medium severity and risk score of 9000.' - ); + expect(buildReasonMessageUtil({ rule })).toMatchInlineSnapshot(`""`); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts index 4917cdbd29170..e93a45bd13246 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { getOr } from 'lodash/fp'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { SignalSourceHit } from './types'; @@ -14,54 +15,118 @@ export interface BuildReasonMessageArgs { mergedDoc?: SignalSourceHit; } +export interface BuildReasonMessageUtilArgs extends BuildReasonMessageArgs { + type?: 'eql' | 'ml' | 'query' | 'threatMatch' | 'threshold'; +} + export type BuildReasonMessage = (args: BuildReasonMessageArgs) => string; +interface ReasonFields { + destinationAddress?: string | string[] | null; + destinationPort?: string | string[] | null; + eventCategory?: string | string[] | null; + fileName?: string | string[] | null; + hostName?: string | string[] | null; + processName?: string | string[] | null; + processParentName?: string | string[] | null; + sourceAddress?: string | string[] | null; + sourcePort?: string | string[] | null; + userName?: string | string[] | null; +} +const getFieldsFromDoc = (mergedDoc: SignalSourceHit) => { + const reasonFields: ReasonFields = {}; + const docToUse = mergedDoc?.fields || mergedDoc; + + reasonFields.destinationAddress = getOr(null, 'destination.address', docToUse); + reasonFields.destinationPort = getOr(null, 'destination.port', docToUse); + reasonFields.eventCategory = getOr(null, 'event.category', docToUse); + reasonFields.fileName = getOr(null, 'file.name', docToUse); + reasonFields.hostName = getOr(null, 'host.name', docToUse); + reasonFields.processName = getOr(null, 'process.name', docToUse); + reasonFields.processParentName = getOr(null, 'process.parent.name', docToUse); + reasonFields.sourceAddress = getOr(null, 'source.address', docToUse); + reasonFields.sourcePort = getOr(null, 'source.port', docToUse); + reasonFields.userName = getOr(null, 'user.name', docToUse); + + return reasonFields; +}; /** * Currently all security solution rule types share a common reason message string. This function composes that string * In the future there may be different configurations based on the different rule types, so the plumbing has been put in place * to more easily allow for this in the future. * @export buildCommonReasonMessage - is only exported for testing purposes, and only used internally here. */ -export const buildCommonReasonMessage = ({ rule, mergedDoc }: BuildReasonMessageArgs) => { - if (!rule) { +export const buildReasonMessageUtil = ({ rule, mergedDoc }: BuildReasonMessageUtilArgs) => { + if (!rule || !mergedDoc) { // This should never happen, but in case, better to not show a malformed string return ''; } - let hostName; - let userName; - if (mergedDoc?.fields) { - hostName = mergedDoc.fields['host.name'] != null ? mergedDoc.fields['host.name'] : hostName; - userName = mergedDoc.fields['user.name'] != null ? mergedDoc.fields['user.name'] : userName; - } + const { + destinationAddress, + destinationPort, + eventCategory, + fileName, + hostName, + processName, + processParentName, + sourceAddress, + sourcePort, + userName, + } = getFieldsFromDoc(mergedDoc); + + const fieldPresenceTracker = { hasFieldOfInterest: false }; - const isFieldEmpty = (field: string | string[] | undefined | null) => - !field || !field.length || (field.length === 1 && field[0] === '-'); + const getFieldTemplateValue = ( + field: string | string[] | undefined | null, + isFieldOfInterest?: boolean + ): string | null => { + if (!field || !field.length || (field.length === 1 && field[0] === '-')) return null; + if (isFieldOfInterest && !fieldPresenceTracker.hasFieldOfInterest) + fieldPresenceTracker.hasFieldOfInterest = true; + return Array.isArray(field) ? field.join(', ') : field; + }; return i18n.translate('xpack.securitySolution.detectionEngine.signals.alertReasonDescription', { - defaultMessage: - 'Alert {alertName} created with a {alertSeverity} severity and risk score of {alertRiskScore}{userName, select, null {} other {{whitespace}by {userName}} }{hostName, select, null {} other {{whitespace}on {hostName}} }.', + defaultMessage: `{eventCategory, select, null {} other {{eventCategory}{whitespace}}}event\ +{hasFieldOfInterest, select, false {} other {{whitespace}with}}\ +{processName, select, null {} other {{whitespace}process {processName},} }\ +{processParentName, select, null {} other {{whitespace}parent process {processParentName},} }\ +{fileName, select, null {} other {{whitespace}file {fileName},} }\ +{sourceAddress, select, null {} other {{whitespace}source {sourceAddress}}}{sourcePort, select, null {} other {:{sourcePort},}}\ +{destinationAddress, select, null {} other {{whitespace}destination {destinationAddress}}}{destinationPort, select, null {} other {:{destinationPort},}}\ +{userName, select, null {} other {{whitespace}by {userName}} }\ +{hostName, select, null {} other {{whitespace}on {hostName}} } \ +created {alertSeverity} alert {alertName}.`, values: { alertName: rule.name, alertSeverity: rule.severity, - alertRiskScore: rule.risk_score, - hostName: isFieldEmpty(hostName) ? 'null' : hostName, - userName: isFieldEmpty(userName) ? 'null' : userName, + destinationAddress: getFieldTemplateValue(destinationAddress, true), + destinationPort: getFieldTemplateValue(destinationPort, true), + eventCategory: getFieldTemplateValue(eventCategory), + fileName: getFieldTemplateValue(fileName, true), + hostName: getFieldTemplateValue(hostName), + processName: getFieldTemplateValue(processName, true), + processParentName: getFieldTemplateValue(processParentName, true), + sourceAddress: getFieldTemplateValue(sourceAddress, true), + sourcePort: getFieldTemplateValue(sourcePort, true), + userName: getFieldTemplateValue(userName), + hasFieldOfInterest: fieldPresenceTracker.hasFieldOfInterest, // Tracking if we have any fields to show the 'with' word whitespace: ' ', // there isn't support for the unicode /u0020 for whitespace, and leading spaces are deleted, so to prevent double-whitespace explicitly passing the space in. }, }); }; export const buildReasonMessageForEqlAlert = (args: BuildReasonMessageArgs) => - buildCommonReasonMessage({ ...args }); + buildReasonMessageUtil({ ...args, type: 'eql' }); export const buildReasonMessageForMlAlert = (args: BuildReasonMessageArgs) => - buildCommonReasonMessage({ ...args }); + buildReasonMessageUtil({ ...args, type: 'ml' }); export const buildReasonMessageForQueryAlert = (args: BuildReasonMessageArgs) => - buildCommonReasonMessage({ ...args }); + buildReasonMessageUtil({ ...args, type: 'query' }); export const buildReasonMessageForThreatMatchAlert = (args: BuildReasonMessageArgs) => - buildCommonReasonMessage({ ...args }); + buildReasonMessageUtil({ ...args, type: 'threatMatch' }); export const buildReasonMessageForThresholdAlert = (args: BuildReasonMessageArgs) => - buildCommonReasonMessage({ ...args }); + buildReasonMessageUtil({ ...args, type: 'threshold' }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index e98d9fff04a0c..c3c83f6be72c8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -8,7 +8,13 @@ import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; // @ts-expect-error import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; -import { EuiEmptyPrompt, EuiLoadingContent, EuiPanel } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiLoadingContent, +} from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; @@ -80,6 +86,16 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ flex-direction: column; `; +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` + overflow: hidden; + margin: 0; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; export interface TGridIntegratedProps { @@ -309,56 +325,61 @@ const TGridIntegratedComponent: React.FC = ({ {!graphEventId && graphOverlay == null && ( - <> - {totalCountMinusDeleted === 0 && loading === false && ( - - - - } - titleSize="s" - body={ -

- -

- } - /> - )} - {totalCountMinusDeleted > 0 && ( - - )} - + + + {totalCountMinusDeleted === 0 && loading === false && ( + + + + } + titleSize="s" + body={ +

+ +

+ } + /> + )} + {totalCountMinusDeleted > 0 && ( + + )} +
+
)} )} diff --git a/x-pack/plugins/timelines/public/store/t_grid/model.ts b/x-pack/plugins/timelines/public/store/t_grid/model.ts index 0972189b38b30..dc6945d3fe3ad 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/model.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/model.ts @@ -92,11 +92,15 @@ export type TGridModelForTimeline = Pick< | 'dataProviders' | 'dateRange' | 'deletedEventIds' + | 'documentType' | 'excludedRowRendererIds' | 'expandedDetail' | 'filters' + | 'filterManager' + | 'footerText' | 'graphEventId' | 'kqlQuery' + | 'queryFields' | 'id' | 'indexNames' | 'isLoading' @@ -104,11 +108,14 @@ export type TGridModelForTimeline = Pick< | 'itemsPerPage' | 'itemsPerPageOptions' | 'loadingEventIds' + | 'loadingText' + | 'selectAll' | 'showCheckboxes' | 'sort' | 'selectedEventIds' | 'savedObjectId' | 'title' + | 'unit' | 'version' >; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7379c4b9c813f..209b763686f7e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7969,8 +7969,6 @@ "xpack.enterpriseSearch.appSearch.documentCreation.api.example": "実行中のAPIを表示するには、コマンドラインまたはクライアントライブラリを使用して、次の要求の例で実験することができます。", "xpack.enterpriseSearch.appSearch.documentCreation.api.title": "APIでインデックス", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.api": "API からインデックス", - "xpack.enterpriseSearch.appSearch.documentCreation.buttons.betaTitle": "ベータ", - "xpack.enterpriseSearch.appSearch.documentCreation.buttons.betaTooltip": "Elastic Crawler は GA ではありません。不具合が発生したら報告してください。", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.crawl": "Crawler を使用", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.file": "JSON ファイルのアップロード", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.text": "JSON の貼り付け", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b7dbcafd6986e..2ba41ff25b510 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8216,8 +8216,6 @@ "xpack.enterpriseSearch.appSearch.documentCreation.api.example": "要了解如何使用 API,可以在下面通过命令行或客户端库试用示例请求。", "xpack.enterpriseSearch.appSearch.documentCreation.api.title": "按 API 索引", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.api": "从 API 索引", - "xpack.enterpriseSearch.appSearch.documentCreation.buttons.betaTitle": "公测版", - "xpack.enterpriseSearch.appSearch.documentCreation.buttons.betaTooltip": "Elastic 网络爬虫并非 GA 版。请通过报告错误来帮助我们。", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.crawl": "使用网络爬虫", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.file": "上传 JSON 文件", "xpack.enterpriseSearch.appSearch.documentCreation.buttons.text": "粘贴 JSON", diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index d43fb2e7d835f..fe67decd7d191 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -23,7 +23,8 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { const retry = getService('retry'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('delete', () => { + // FLAKY https://github.com/elastic/kibana/issues/111001 + describe.skip('delete', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 3454ef5c94d9f..2126e7383e321 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -17,7 +17,8 @@ export default function createFindTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('find', () => { + // FLAKY https://github.com/elastic/kibana/issues/111022 + describe.skip('find', () => { const objectRemover = new ObjectRemover(supertest); afterEach(() => objectRemover.removeAll()); diff --git a/x-pack/test/api_integration/apis/metrics_ui/constants.ts b/x-pack/test/api_integration/apis/metrics_ui/constants.ts index a4c5705ab1a10..f0ba9b4c368d5 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/constants.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/constants.ts @@ -28,4 +28,14 @@ export const DATES = { max: 1564083493080, }, }, + 'alert-test-data': { + gauge: { + min: 1609459200000, // '2022-01-01T00:00:00Z' + max: 1609462800000, // '2021-01-01T01:00:00Z' + }, + rate: { + min: 1609545600000, // '2021-01-02T00:00:00Z' + max: 1609545900000, // '2021-01-02T00:05:00Z' + }, + }, }; diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js index 861d82733a0fa..dfba4ee0985ba 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -18,5 +18,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./metrics_explorer')); loadTestFile(require.resolve('./ip_to_hostname')); loadTestFile(require.resolve('./http_source')); + loadTestFile(require.resolve('./metric_threshold_alert')); }); } diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts new file mode 100644 index 0000000000000..28910bbc6b0c8 --- /dev/null +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts @@ -0,0 +1,322 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { InfraSource } from '../../../../plugins/infra/common/source_configuration/source_configuration'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + evaluateAlert, + EvaluatedAlertParams, +} from '../../../../plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert'; +import { + Aggregators, + CountMetricExpressionParams, + NonCountMetricExpressionParams, +} from '../../../../plugins/infra/server/lib/alerting/metric_threshold/types'; +import { Comparator } from '../../../../plugins/infra/server/lib/alerting/common/types'; +import { DATES } from './constants'; + +const { gauge, rate } = DATES['alert-test-data']; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const esClient = getService('es'); + + const baseParams: EvaluatedAlertParams = { + groupBy: void 0, + filterQuery: void 0, + criteria: [ + { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: Comparator.GT_OR_EQ, + aggType: Aggregators.SUM, + metric: 'value', + }, + ], + }; + + const configuration: InfraSource['configuration'] = { + name: 'Default', + description: '', + logIndices: { + type: 'index_pattern', + indexPatternId: 'some-test-id', + }, + metricAlias: 'alerts-test-data', + inventoryDefaultView: 'default', + metricsExplorerDefaultView: 'default', + anomalyThreshold: 70, + fields: { + container: 'container.id', + host: 'host.name', + pod: 'kubernetes.od.uid', + tiebreaker: '_doc', + timestamp: '@timestamp', + message: ['message'], + }, + logColumns: [ + { + timestampColumn: { + id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f', + }, + }, + { + fieldColumn: { + id: ' eb9777a8-fcd3-420e-ba7d-172fff6da7a2', + field: 'event.dataset', + }, + }, + { + messageColumn: { + id: 'b645d6da-824b-4723-9a2a-e8cece1645c0', + }, + }, + ], + }; + + describe('Metric Threshold Alerts Executor', () => { + before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/alerts_test_data')); + after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/alerts_test_data')); + + describe('with gauge data', () => { + describe('without groupBy', () => { + it('should alert on document count', async () => { + const params = { + ...baseParams, + criteria: [ + { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: Comparator.GT_OR_EQ, + aggType: Aggregators.COUNT, + } as CountMetricExpressionParams, + ], + }; + const timeFrame = { end: gauge.max }; + const results = await evaluateAlert(esClient, params, configuration, timeFrame); + expect(results).to.eql([ + { + '*': { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'count', + metric: 'Document count', + currentValue: 4, + timestamp: '2021-01-01T00:55:00.000Z', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + }, + ]); + }); + it('should alert on the last value when the end date is the same as the last event', async () => { + const params = { ...baseParams }; + const timeFrame = { end: gauge.max }; + const results = await evaluateAlert(esClient, params, configuration, timeFrame); + expect(results).to.eql([ + { + '*': { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: 1, + timestamp: '2021-01-01T00:55:00.000Z', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + }, + ]); + }); + }); + describe('with groupBy', () => { + it('should alert on document count', async () => { + const params = { + ...baseParams, + groupBy: ['env'], + criteria: [ + { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: Comparator.GT_OR_EQ, + aggType: Aggregators.COUNT, + } as CountMetricExpressionParams, + ], + }; + const timeFrame = { end: gauge.max }; + const results = await evaluateAlert(esClient, params, configuration, timeFrame); + expect(results).to.eql([ + { + dev: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'count', + metric: 'Document count', + currentValue: 2, + timestamp: '2021-01-01T00:55:00.000Z', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + prod: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'count', + metric: 'Document count', + currentValue: 2, + timestamp: '2021-01-01T00:55:00.000Z', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + }, + ]); + }); + it('should alert on the last value when the end date is the same as the last event', async () => { + const params = { + ...baseParams, + groupBy: ['env'], + }; + const timeFrame = { end: gauge.max }; + const results = await evaluateAlert(esClient, params, configuration, timeFrame); + expect(results).to.eql([ + { + dev: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: 0, + timestamp: '2021-01-01T00:55:00.000Z', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + prod: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: 1, + timestamp: '2021-01-01T00:55:00.000Z', + shouldFire: [true], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + }, + ]); + }); + }); + }); + + describe('with rate data', () => { + describe('without groupBy', () => { + it('should alert on rate', async () => { + const params = { + ...baseParams, + criteria: [ + { + timeSize: 1, + timeUnit: 'm', + threshold: [0.5], + comparator: Comparator.GT_OR_EQ, + aggType: Aggregators.RATE, + metric: 'value', + } as NonCountMetricExpressionParams, + ], + }; + const timeFrame = { end: rate.max }; + const results = await evaluateAlert(esClient, params, configuration, timeFrame); + expect(results).to.eql([ + { + '*': { + timeSize: 1, + timeUnit: 'm', + threshold: [0.5], + comparator: '>=', + aggType: 'rate', + metric: 'value', + currentValue: 0.6666666666666666, + timestamp: '2021-01-02T00:04:00.000Z', + shouldFire: [false, false, false, false, true], + shouldWarn: [false], + isNoData: [true, false, false, false, false], + isError: false, + }, + }, + ]); + }); + }); + describe('with groupBy', () => { + it('should warn but not fire on rate', async () => { + const params = { + ...baseParams, + groupBy: 'env', + criteria: [ + { + timeSize: 1, + timeUnit: 'm', + threshold: [1], + comparator: Comparator.GT_OR_EQ, + warningThreshold: [0.5], + warningComparator: Comparator.GT_OR_EQ, + aggType: Aggregators.RATE, + metric: 'value', + } as NonCountMetricExpressionParams, + ], + }; + const timeFrame = { end: rate.max }; + const results = await evaluateAlert(esClient, params, configuration, timeFrame); + expect(results).to.eql([ + { + dev: { + timeSize: 1, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + warningThreshold: [0.5], + warningComparator: '>=', + aggType: 'rate', + metric: 'value', + currentValue: 0.6666666666666666, + timestamp: '2021-01-02T00:04:00.000Z', + shouldFire: [false, false, false, false, false], + shouldWarn: [false, false, false, false, true], + isNoData: [true, false, false, false, false], + isError: false, + }, + }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts index 31d4bd147f526..90b815d4d0530 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts @@ -48,7 +48,9 @@ export default function ({ getService }: FtrProviderContext) { }); expect(result.hits).to.be.ok(); - expect(result.aggregations).to.be.ok(); + if (aggType !== 'count') { + expect(result.aggregations).to.be.ok(); + } }); } it('should work with a filterQuery', async () => { diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 394672ac07fc5..e44d0cd10e9f2 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -77,6 +77,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./indices')); loadTestFile(require.resolve('./job_validation')); + loadTestFile(require.resolve('./job_audit_messages')); loadTestFile(require.resolve('./jobs')); loadTestFile(require.resolve('./modules')); loadTestFile(require.resolve('./results')); diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts new file mode 100644 index 0000000000000..d085f360859ec --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { omit } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getJobConfig } from './index'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + let notificationIndices: string[] = []; + + describe('clear_messages', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const jobConfig of getJobConfig(2)) { + await ml.api.createAnomalyDetectionJob(jobConfig); + } + + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + notificationIndices = body.notificationIndices; + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should mark audit messages as cleared for provided job', async () => { + const timestamp = Date.now(); + + const { body } = await supertest + .put(`/api/ml/job_audit_messages/clear_messages`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ + jobId: 'test_get_job_audit_messages_1', + notificationIndices, + }) + .expect(200); + + expect(body.success).to.eql(true); + expect(body.last_cleared).to.be.above(timestamp); + + const { body: getBody } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(getBody.messages.length).to.eql(1); + + expect(omit(getBody.messages[0], 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + cleared: true, + }); + }); + + it('should not mark audit messages as cleared for the user with ML read permissions', async () => { + const { body } = await supertest + .put(`/api/ml/job_audit_messages/clear_messages`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send({ + jobId: 'test_get_job_audit_messages_2', + notificationIndices, + }) + .expect(403); + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + + const { body: getBody } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_2`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(getBody.messages[0].cleared).to.not.eql(true); + }); + + it('should not mark audit messages as cleared for unauthorized user', async () => { + const { body } = await supertest + .put(`/api/ml/job_audit_messages/clear_messages`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send({ + jobId: 'test_get_job_audit_messages_2', + notificationIndices, + }) + .expect(403); + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + + const { body: getBody } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_2`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(getBody.messages[0].cleared).to.not.eql(true); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts new file mode 100644 index 0000000000000..2211103b2d404 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { omit, keyBy } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { getJobConfig } from './index'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('get_job_audit_messages', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const jobConfig of getJobConfig(2)) { + await ml.api.createAnomalyDetectionJob(jobConfig); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch all audit messages', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.messages.length).to.eql(2); + + const messagesDict = keyBy(body.messages, 'job_id'); + + expect(omit(messagesDict.test_get_job_audit_messages_2, 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_2', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(omit(messagesDict.test_get_job_audit_messages_1, 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); + }); + + it('should fetch audit messages for specified job', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.messages.length).to.eql(1); + expect(omit(body.messages[0], 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); + }); + + it('should fetch audit messages for user with ML read permissions', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.messages.length).to.eql(1); + expect(omit(body.messages[0], 'timestamp')).to.eql({ + job_id: 'test_get_job_audit_messages_1', + message: 'Job created', + level: 'info', + node_name: 'node-01', + job_type: 'anomaly_detector', + }); + expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); + }); + + it('should not allow to fetch audit messages for unauthorized user', async () => { + const { body } = await supertest + .get(`/api/ml/job_audit_messages/messages/test_get_job_audit_messages_1`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/index.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/index.ts new file mode 100644 index 0000000000000..4779a3a181e3b --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MlJob } from '@elastic/elasticsearch/api/types'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('job_audit_messages', function () { + loadTestFile(require.resolve('./get_job_audit_messages')); + loadTestFile(require.resolve('./clear_messages')); + }); +} + +export const getJobConfig = (numOfJobs: number) => { + return new Array(numOfJobs).fill(null).map( + (v, i) => + (({ + job_id: `test_get_job_audit_messages_${i + 1}`, + description: 'job_audit_messages', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + { + function: 'min', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + } as unknown) as MlJob) + ); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts index 58df5bc3ff9e1..496781dbb985f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -193,7 +193,7 @@ export default ({ getService }: FtrProviderContext) => { index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs', depth: 0, }, - reason: `Alert Test ML rule created with a critical severity and risk score of 50 by root on mothra.`, + reason: `event with process store, by root on mothra created critical alert Test ML rule.`, original_time: '2020-11-16T22:58:08.000Z', }, all_field_values: [ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index f985cdfecc465..0aad3c699805a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -287,7 +287,8 @@ export default ({ getService }: FtrProviderContext) => { depth: 0, }, ], - reason: `Alert Query with a rule id created with a high severity and risk score of 55 by root on zeek-sensor-amsterdam.`, + reason: + 'user-login event by root on zeek-sensor-amsterdam created high alert Query with a rule id.', rule: fullSignal.signal.rule, status: 'open', }, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index b90ceb3dde9cc..c954d8aa5721d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -362,7 +362,8 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { - reason: `Alert Signal Testing Query created with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, + reason: + 'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.', rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -497,7 +498,8 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { - reason: `Alert Signal Testing Query created with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, + reason: + 'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.', rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -662,7 +664,8 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { - reason: `Alert Signal Testing Query created with a high severity and risk score of 1 by root on zeek-sensor-amsterdam.`, + reason: + 'anomoly event with process bro, by root on zeek-sensor-amsterdam created high alert Signal Testing Query.', rule: fullSignal.signal.rule, group: fullSignal.signal.group, original_time: fullSignal.signal.original_time, @@ -753,7 +756,8 @@ export default ({ getService }: FtrProviderContext) => { status: 'open', depth: 2, group: source.signal.group, - reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`, + reason: + 'event by root on zeek-sensor-amsterdam created high alert Signal Testing Query.', rule: source.signal.rule, ancestors: [ { @@ -872,7 +876,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', - reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`, + reason: 'event created high alert Signal Testing Query.', rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1010,7 +1014,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', - reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`, + reason: `event created high alert Signal Testing Query.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1094,7 +1098,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', - reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`, + reason: `event created high alert Signal Testing Query.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1692,7 +1696,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', - reason: `Alert boot created with a high severity and risk score of 1 on zeek-sensor-amsterdam.`, + reason: `event on zeek-sensor-amsterdam created high alert boot.`, rule: { ...fullSignal.signal.rule, name: 'boot', diff --git a/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz new file mode 100644 index 0000000000000..1c76205f4caa2 Binary files /dev/null and b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/infra/alerts_test_data/mappings.json b/x-pack/test/functional/es_archives/infra/alerts_test_data/mappings.json new file mode 100644 index 0000000000000..10ff43edb7d88 --- /dev/null +++ b/x-pack/test/functional/es_archives/infra/alerts_test_data/mappings.json @@ -0,0 +1,28 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "alerts-test-data", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "env": { + "ignore_above": 256, + "type": "keyword" + }, + "value": { + "type": "long" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file