diff --git a/.eslintrc.js b/.eslintrc.js index 6155d182f7cd7..60d26cbfbab73 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,9 +49,9 @@ const ELASTIC_LICENSE_HEADER = ` */ `; -const allMochaRules = {}; +const allMochaRulesOff = {}; Object.keys(require('eslint-plugin-mocha').rules).forEach(k => { - allMochaRules['mocha/' + k] = 'off'; + allMochaRulesOff['mocha/' + k] = 'off'; }); module.exports = { @@ -524,7 +524,7 @@ module.exports = { */ { files: ['test/harden/*.js'], - rules: allMochaRules, + rules: allMochaRulesOff, }, /** diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md new file mode 100644 index 0000000000000..99f779c288f5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/v8_breaking_change.md @@ -0,0 +1,36 @@ +--- +name: 8.0 Breaking change +about: Breaking changes from 7.x -> 8.0 +title: "[Breaking change]" +labels: Team:Elasticsearch UI, Feature:Upgrade Assistant +assignees: '' + +--- + +## Change description + +**Which release will ship the breaking change?** + + + +**Describe the change. How will it manifest to users?** + +**What percentage of users will be affected?** + + + +**What can users to do to address the change manually?** + + + +**How could we make migration easier with the Upgrade Assistant?** + +**Are there any edge cases?** + +## Test Data + +Provide test data. We can’t build a solution without data to test it against. + +## Cross links + +Cross-link to relevant [Elasticsearch breaking changes](https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html). \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 742aec1d4e7ab..d43da6e0bee04 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -kibanaPipeline(timeoutMinutes: 135) { +kibanaPipeline(timeoutMinutes: 135, checkPrChanges: true) { githubPr.withDefaultPrComments { catchError { retryable.enable() diff --git a/docs/developer/visualize/development-create-visualization.asciidoc b/docs/developer/visualize/development-create-visualization.asciidoc deleted file mode 100644 index e38b76471ab25..0000000000000 --- a/docs/developer/visualize/development-create-visualization.asciidoc +++ /dev/null @@ -1,463 +0,0 @@ -[[development-create-visualization]] -=== Developing Visualizations - -This is a short description of functions and interfaces provided. For more information you should check the kibana -source code and the existing visualizations provided with it. - -- <> -* <> -* <> -- <> -* <> -* <> -- <> -* <> -* <> -* <> -- <> -* <> -* <> -* <> -- <> -* <> -- <> - -[[development-visualization-factory]] -=== Visualization Factory - -Use the `VisualizationFactory` to create a new visualization. -The creation-methods create a new visualization tied to the underlying rendering technology. -You should also register the visualization with `VisTypesRegistryProvider`. - -["source","js"] ------------ -import { VisFactoryProvider } from 'ui/vis/vis_factory'; -import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; - -const MyNewVisType = (Private) => { - const VisFactory = Private(VisFactoryProvider); - - return VisFactory.createBaseVisualization({ - name: 'my_new_vis', - title: 'My New Vis', - icon: 'my_icon', - description: 'Cool new chart', - ... - }); -} - -VisTypesRegistryProvider.register(MyNewVisType); ------------ - -The list of common parameters: - -- *name*: unique visualization name, only lowercase letters and underscore -- *title*: title of your visualization as displayed in kibana -- *icon*: the https://elastic.github.io/eui/#/display/icons[EUI icon] type to use for this visualization -- *image*: instead of an icon you can provide a SVG image (imported) -- *description*: description of your visualization as shown in kibana -- *hidden*: if set to true, will hide the type from showing up in the visualization wizard -- *visConfig*: object holding visualization parameters -- *visConfig.defaults*: object holding default visualization configuration -- *visualization*: A constructor function for a Visualization. -- *requestHandler*: one of the available request handlers or a for a custom request handler -- *responseHandler*: one of the available response handlers or a for a custom response handler -- *editor*: Editor class for custom one -- *editorConfig*: object holding editor parameters -- *options.showTimePicker*: show or hide time filter (defaults to true) -- *options.showQueryBar*: show or hide query bar (defaults to true) -- *options.showFilterBar*: show or hide filter bar (defaults to true) -- *options.showIndexSelection*: show or hide index selection (defaults to true) -- *stage*: Set this to "experimental" to mark your visualization as experimental. -Experimental visualizations can also be disabled from the advanced settings. (defaults to "production") -- *feedbackMessage*: You can provide a message (which can contain HTML), that will be appended -to the experimental notification in visualize, if your visualization is experimental or in lab mode. - - -Each of the factories have some of the custom parameters, which will be described below. - -[[development-base-visualization-type]] -==== Base Visualization Type -The base visualization type does not make any assumptions about the rendering technology you are going to use and -works with pure JavaScript. It is the visualization type we recommend to use. - -You need to provide a type with a constructor function, a render method which will be called every time -options or data change, and a destroy method which will be called to cleanup. - -The render function receives the data object and status object which tells what actually changed. -Render function needs to return a promise, which should be resolved once the visualization is done rendering. - -The status object provides information about changes since the previous render call. -Due to performance reasons you need to opt-in for each status change, that you want -to be informed about by Kibana. This is done by using the `requiresUpdateStatus` key -in your visualization registration object. You pass it an array, that contains all -the status updates you want to receive. By default none of it will be calculated. - -The following snippet shows explain all available status updates. You should only -activate those changes, that you actually use in your `render` method. - -["source","js"] ------------ -import { Status } from 'ui/vis/update_status'; - -// ... -return VisFactory.createBaseVisualization({ - // ... - requiresUpdateStatus: [ - // Check for changes in the aggregation configuration for the visualization - Status.AGGS, - // Check for changes in the actual data returned from Elasticsearch - Status.DATA, - // Check for changes in the parameters (configuration) for the visualization - Status.PARAMS, - // Check if the visualization has changes its size - Status.RESIZE, - // Check if the time range for the visualization has been changed - Status.TIME, - // Check if the UI state of the visualization has been changed - Status.UI_STATE - ] -}); ------------ - -If you activate any of these status updates, the `status` object passed as second -parameter to the `render` method will contain a key for that status (e.g. `status[Status.DATA]`), -that is either `true` if a change has been detected or `false` otherwise. - - -image::images/visualize-flow.png[Main Flow] - -- Your visualizations constructor will get called with `vis` object and the DOM-element to which it should render. -At this point you should prepare everything for rendering, but not render yet -- `` component monitors `appState`, `uiState` and `vis` for changes -- on changes the ``-directive will call your `requestHandler`. -Implementing a request handler is optional, as you might use one of the provided ones. -- response from `requestHandler` will get passed to `responseHandler`. It should convert raw data to something that -can be consumed by visualization. Implementing `responseHandler` is optional, as you might use of of the provided ones. -- On new data from the `responseHandler` or on when the size of the surrounding DOM-element has changed, -your visualization `render`-method gets called. It needs to return a promise which resolves once the visualization -is done rendering. -- the visualization should call `vis.updateState()` any time something has changed that requires to -re-render or fetch new data. - -["source","js"] ------------ -import { VisFactoryProvider } from 'ui/vis/vis_factory'; -import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; - -class MyVisualization { - constructor(el, vis) { - this.el = el; - this.vis = vis; - } - async render(visData, status) { - ... - return 'done rendering'; - } - destroy() { - console.log('destroying'); - } -} - -const MyNewVisType = (Private) => { - const VisFactory = Private(VisFactoryProvider); - - return VisFactory.createBaseVisualization({ - name: 'my_new_vis', - title: 'My New Vis', - icon: 'my_icon', - description: 'Cool new chart', - visualization: MyVisualization - }); -} - -VisTypesRegistryProvider.register(MyNewVisType); ------------ - -[[development-react-visualization-type]] -==== React Visualization Type -React visualization type assumes you are using React as your rendering technology. -Just pass in a React component to `visConfig.component`. - -The visualization will receive `vis`, `appState`, `updateStatus` and `visData` as props. -It also has a `renderComplete` property, which needs to be called once the rendering has completed. - -["source","js"] ------------ -import { ReactComponent } from './my_react_component'; - -const MyNewVisType = (Private) => { - const VisFactory = Private(VisFactoryProvider); - - return VisFactory.createReactVisualization({ - name: 'my_new_vis', - title: 'My New Vis', - icon: 'my_icon', - description: 'Cool new chart', - visConfig: { - component: ReactComponent - } - }); -} ------------ - -[[development-vis-editors]] -=== Visualization Editors -By default, visualizations will use the `default` editor. -This is the sidebar editor you see in many of the Kibana visualizations. You can also write your own editor. - -[[development-default-editor]] -==== `default` editor controller -The default editor controller receives an `optionsTemplate` or `optionTabs` parameter. -These tabs should be React components. - -["source","js"] ------------ -{ - name: 'my_new_vis', - title: 'My New Vis', - icon: 'my_icon', - description: 'Cool new chart', - editorConfig: { - optionsTemplate: MyReactComponent // or if multiple tabs are required: - optionTabs: [ - { title: 'tab 3', editor: MyReactComponent } - ] - } - } ------------ - -[[development-custom-editor]] -==== custom editor controller -You can create a custom editor controller. To do so pass an Editor object (the same format as VisController class). -You can make your controller take extra configuration which is passed to the editorConfig property. - -["source","js"] ------------ -import { VisFactoryProvider } from 'ui/vis/vis_factory'; - -class MyEditorController { - constructor(el, vis) { - this.el = el; - this.vis = vis; - this.config = vis.type.editorConfig; - } - async render(visData) { - console.log(this.config.my); - ... - return 'done rendering'; - } - destroy() { - console.log('destroying'); - } -} - -const MyNewVisType = (Private) => { - const VisFactory = Private(VisFactoryProvider); - - return VisFactory.createAngularVisualization({ - name: 'my_new_vis', - title: 'My New Vis', - icon: 'my_icon', - description: 'Cool new chart', - editor: MyEditorController, - editorConfig: { my: 'custom config' } - }); -} - -VisTypesRegistryProvider.register(MyNewVisType); ------------ - -[[development-visualization-request-handlers]] -=== Visualization Request Handlers -Request handler gets called when one of the following keys on AppState change: -`vis`, `query`, `filters` or `uiState` and when the time filter is updated. On top -of that it will also get called on force refresh. - -By default visualizations will use the `courier` request handler. They can also choose to use any of the other provided -request handlers. It is also possible to define your own request handler -(which you can then register to be used by other visualizations). - -[[development-default-request-handler]] -==== courier request handler -'courier' is the default request handler which works with the 'default' side bar editor. - -[[development-none-request-handler]] -==== `none` request handler -Using 'none' as your request handles means your visualization does not require any data to be requested. - -[[development-custom-request-handler]] -==== custom request handler -You can define your custom request handler by providing a function with the following signature: -`function (vis, { uiState, appState, timeRange }) { ... }` - -The `timeRange` will be an object with a `from` and `to` key, that can contain -datemath expressions, like `now-7d`. You can use the `datemath` library to parse -them. - -This function must return a promise, which should get resolved with new data that will be passed to responseHandler. - -It's up to function to decide when it wants to issue a new request or return previous data -(if none of the objects relevant to the request handler changed). - -["source","js"] ------------ -import { VisFactoryProvider } from 'ui/vis/vis_factory'; - -const myRequestHandler = async (vis, { appState, uiState, timeRange }) => { - const data = ... parse ... - return data; -}; - -const MyNewVisType = (Private) => { - const VisFactory = Private(VisFactoryProvider); - - return VisFactory.createAngularVisualization({ - name: 'my_new_vis', - title: 'My New Vis', - icon: 'my_icon', - description: 'Cool new chart', - requestHandler: myRequestHandler - }); -} - -VisTypesRegistryProvider.register(MyNewVisType); ------------ - -[[development-visualization-response-handlers]] -=== Visualization Response Handlers -The response handler is a function that receives the data from a request handler, as well as an instance of Vis object. -Its job is to convert the data to a format visualization can use. By default 'default' request handler is used -which produces a table representation of the data. The data object will then be passed to visualization. -This response matches the visData property of the directive. - -[[development-default-response-handler]] -==== default response handler -The default response handler converts pure elasticsearch responses into a tabular format. -It is the recommended responseHandler. The response object contains a table property, -which is an array of all the tables in the response. Each of the table objects has two properties: - -- `columns`: array of column objects, where each column object has a title property and an aggConfig property -- `rows`: array of rows, where each row is an array of non formatted cell values - -Here is an example of a response with 1 table, 3 columns and 2 rows: - -["source","js"] ------------ -{ - tables: [{ - columns: [{ - title: 'column1', - aggConfig: ... - },{ - title: 'column2', - aggConfig: ... - },{ - title: 'column3', - aggConfig: ... - }], - rows: [ - [ '404', 1262, 12.5 ] - [ '200', 343546, 60.1 ] - ] - }]; -} ------------ - -[[development-none-response-handler]] -==== none response handler -None response handler is an identity function, which will return the same data it receives. - -[[development-custom-response-handler]] -==== custom response handler -You can define your custom response handler by providing a function with the following definition: -'function (vis, response) { ... }'. - -Function should return the transformed data object that visualization can consume. - -["source","js"] ------------ -import { VisFactoryProvider } from 'ui/vis/vis_factory'; - -const myResponseHandler = (vis, response) => { - // transform the response (based on vis object?) - const response = ... transform data ...; - return response; -}; - -const MyNewVisType(Private) => { - const VisFactory = Private(VisFactoryProvider); - - return VisFactory.createAngularVisualization({ - name: 'my_new_vis', - title: 'My New Vis', - icon: 'my_icon', - description: 'Cool new chart', - responseHandler: myResponseHandler - }); -} - -VisTypesRegistryProvider.register(MyNewVisType); ------------ - -[[development-vis-object]] -=== Vis object -The `vis` object holds the visualization state and is the window into kibana: - -- *vis.params*: holds the visualization parameters -- *vis.indexPattern*: selected index pattern object -- *vis.getState()*: gets current visualization state -- *vis.updateState()*: updates current state with values from `vis.params` -- *vis.resetState()*: resets `vis.params` to the values in the current state -- *vis.forceReload()*: forces whole cycle (request handler gets called) -- *vis.getUiState()*: gets UI state of visualization -- *vis.uiStateVal(name, val)*: updates a property in UI state -- *vis.isEditorMode()*: returns true if in editor mode -- *vis.API.timeFilter*: allows you to access time filter -- *vis.API.queryFilter*: gives you access to queryFilter -- *vis.API.events.click*: default click handler -- *vis.API.events.brush*: default brush handler - -The visualization gets all its parameters in `vis.params`, which are default values merged with the current state. -If the visualization needs to update the current state, it should update the `vis.params` and call `vis.updateState()` -which will inform about the change, which will call request and response handler and then your -visualization's render method. - -For the parameters that should not be saved with the visualization you should use the UI state. -These hold viewer-specific state, such as popup open/closed, custom colors applied to the series etc. - -You can access the filter bar and time filter through the objects defined on `vis.API` - -[[development-vis-timefilter]] -==== timeFilter - -Update the timefilter time values and call update() method on it to update the time filter - -["source","js"] ------------ - timefilter.time.from = moment(ranges.xaxis.from); - timefilter.time.to = moment(ranges.xaxis.to); - timefilter.time.mode = 'absolute'; - timefilter.update(); ------------ - - -[[development-aggconfig]] -=== AggConfig object - -The AggConfig object represents an aggregation search to Elasticsearch, -plus some additional functionality to manage data-values that belong to this aggregation. -This is primarily used internally in Kibana, but you may find you have a need for it -when writing your own visualization. Here we provide short description of some of the methods on it, -however the best reference would be to actually check the source code. - - -- *fieldFormatter()* : returns a function which will format your value according to field formatters defined on -the field. The type can be either 'text' or 'html'. -- *makeLabel()* : gets the label for the aggregation -- *isFilterable()* : return true if aggregation is filterable (you can then call createFilter) -- *createFilter(bucketKey)* : creates a filter for specific bucket key -- *getValue(bucket)* : gets value for a specific bucket -- *getField()* : gets the field used for this aggregation -- *getFieldDisplayName()* : gets field display name -- *getAggParams()* : gets the arguments to the aggregation diff --git a/docs/developer/visualize/development-embedding-visualizations.asciidoc b/docs/developer/visualize/development-embedding-visualizations.asciidoc deleted file mode 100644 index 1c275e7831f74..0000000000000 --- a/docs/developer/visualize/development-embedding-visualizations.asciidoc +++ /dev/null @@ -1,58 +0,0 @@ -[[development-embedding-visualizations]] -=== Embedding Visualizations - -To embed visualization use the `VisualizeLoader`. - -==== VisualizeLoader - -The `VisualizeLoader` class is the easiest way to embed a visualization into your plugin. -It will take care of loading the data and rendering the visualization. - -To get an instance of the loader, do the following: - -["source","js"] ------------ -import { getVisualizeLoader } from 'ui/visualize/loader'; - -getVisualizeLoader().then((loader) => { - // You now have access to the loader -}); ------------ - -The loader exposes the following methods: - -- `getVisualizationList()`: which returns promise which gets resolved with a list of saved visualizations -- `embedVisualizationWithId(container, savedId, params)`: which embeds visualization by id -- `embedVisualizationWithSavedObject(container, savedObject, params)`: which embeds visualization from saved object - -Depending on which embed method you are using, you either pass in the id of the -saved object for the visualization, or a `savedObject`, that you can retrieve via -the `savedVisualizations` Angular service by its id. The `savedObject` give you access -to the filter and query logic and allows you to attach listeners to the visualizations. -For a more complex use-case you usually want to use that method. - -`container` should be a DOM element (jQuery wrapped or regular DOM element) into which the visualization should be embedded -`params` is a parameter object specifying several parameters, that influence rendering. - -You will find a detailed description of all the parameters in the inline docs -in the {repo}blob/{branch}/src/legacy/ui/public/visualize/loader/types.ts[loader source code]. - -Both methods return an `EmbeddedVisualizeHandler`, that gives you some access -to the visualization. The `embedVisualizationWithSavedObject` method will return -the handler immediately from the method call, whereas the `embedVisualizationWithId` -will return a promise, that resolves with the handler, as soon as the `id` could be -found. It will reject, if the `id` is invalid. - -The returned `EmbeddedVisualizeHandler` itself has the following methods and properties: - -- `destroy()`: destroys the embedded visualization. You MUST call that method when navigating away - or destroying the DOM node you have embedded into. -- `getElement()`: a reference to the jQuery wrapped DOM element, that renders the visualization -- `whenFirstRenderComplete()`: will return a promise, that resolves as soon as the visualization has - finished rendering for the first time -- `addRenderCompleteListener(listener)`: will register a listener to be called whenever - a rendering of this visualization finished (not just the first one) -- `removeRenderCompleteListener(listener)`: removes an event listener from the handler again - -You can find the detailed `EmbeddedVisualizeHandler` documentation in its -{repo}blob/{branch}/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts[source code]. \ No newline at end of file diff --git a/docs/developer/visualize/development-visualize-index.asciidoc b/docs/developer/visualize/development-visualize-index.asciidoc index 1cdeac7540ce4..daefc434e1f18 100644 --- a/docs/developer/visualize/development-visualize-index.asciidoc +++ b/docs/developer/visualize/development-visualize-index.asciidoc @@ -1,21 +1,26 @@ [[development-visualize-index]] == Developing Visualizations -Kibana Visualizations are the easiest way to add additional functionality to Kibana. -This part of documentation is split into two parts. -The first part tells you all you need to know on how to embed existing Kibana Visualizations in your plugin. -The second step explains how to create your own custom visualization. - [IMPORTANT] ============================================== -These pages document internal APIs and are not guaranteed to be supported across future versions of Kibana. -However, these docs will be kept up-to-date to reflect the current implementation of Visualization plugins in Kibana. +These pages document internal APIs and are not guaranteed to be supported across future versions of Kibana. ============================================== -* <> -* <> +The internal APIs for creating custom visualizations are in a state of heavy churn as +they are being migrated to the new Kibana platform, and large refactorings have been +happening across minor releases in the `7.x` series. In particular, in `7.5` and later +we have made significant changes to the legacy APIs as we work to gradually replace them. +As a result, starting in `7.5` we have removed the documentation for the legacy APIs +to prevent confusion. We expect to be able to create new documentation later in `7.x` +when the visualizations plugin has been completed. -include::development-embedding-visualizations.asciidoc[] +We would recommend waiting until later in `7.x` to upgrade your plugins if possible. +If you would like to keep up with progress on the visualizations plugin in the meantime, +here are a few resources: -include::development-create-visualization.asciidoc[] \ No newline at end of file +* The <> documentation, where we try to capture any changes to the APIs as they occur across minors. +* link:https://github.com/elastic/kibana/issues/44121[Meta issue] which is tracking the move of the plugin to the new Kibana platform +* Our link:https://www.elastic.co/blog/join-our-elastic-stack-workspace-on-slack[Elastic Stack workspace on Slack]. +* The {repo}blob/{branch}/src/plugins/visualizations[source code], which will continue to be +the most accurate source of information. diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.addsearchstrategy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.addsearchstrategy.md deleted file mode 100644 index 119e7fbe62536..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.addsearchstrategy.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [addSearchStrategy](./kibana-plugin-plugins-data-public.addsearchstrategy.md) - -## addSearchStrategy variable - -Signature: - -```typescript -addSearchStrategy: (searchStrategy: SearchStrategyProvider) => void -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.defaultsearchstrategy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.defaultsearchstrategy.md deleted file mode 100644 index d6a71cf561bc2..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.defaultsearchstrategy.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [defaultSearchStrategy](./kibana-plugin-plugins-data-public.defaultsearchstrategy.md) - -## defaultSearchStrategy variable - -Signature: - -```typescript -defaultSearchStrategy: SearchStrategyProvider -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.essearchstrategyprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.essearchstrategyprovider.md deleted file mode 100644 index 1394c6b868546..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.essearchstrategyprovider.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [esSearchStrategyProvider](./kibana-plugin-plugins-data-public.essearchstrategyprovider.md) - -## esSearchStrategyProvider variable - -Signature: - -```typescript -esSearchStrategyProvider: TSearchStrategyProvider -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getdefaultquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getdefaultquery.md new file mode 100644 index 0000000000000..5e6627880333e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getdefaultquery.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [getDefaultQuery](./kibana-plugin-plugins-data-public.getdefaultquery.md) + +## getDefaultQuery() function + +Signature: + +```typescript +export declare function getDefaultQuery(language?: QueryLanguage): { + query: string; + language: QueryLanguage; +}; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| language | QueryLanguage | | + +Returns: + +`{ + query: string; + language: QueryLanguage; +}` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md deleted file mode 100644 index 94608e7a86820..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [hasSearchStategyForIndexPattern](./kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md) - -## hasSearchStategyForIndexPattern variable - -Signature: - -```typescript -hasSearchStategyForIndexPattern: (indexPattern: IndexPattern) => boolean -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.indextype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.indextype.md new file mode 100644 index 0000000000000..55b43efc52305 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.indextype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) > [indexType](./kibana-plugin-plugins-data-public.iessearchrequest.indextype.md) + +## IEsSearchRequest.indexType property + +Signature: + +```typescript +indexType?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md index 7a40725a67e5f..ed24ca613cdf6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md @@ -14,5 +14,6 @@ export interface IEsSearchRequest extends IKibanaSearchRequest | Property | Type | Description | | --- | --- | --- | +| [indexType](./kibana-plugin-plugins-data-public.iessearchrequest.indextype.md) | string | | | [params](./kibana-plugin-plugins-data-public.iessearchrequest.params.md) | SearchParams | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 4b85461e64097..ce1375d277b75 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -74,32 +74,26 @@ | [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | | | [SavedQuery](./kibana-plugin-plugins-data-public.savedquery.md) | | -| [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) | | | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | | | [SearchStrategyProvider](./kibana-plugin-plugins-data-public.searchstrategyprovider.md) | | -| [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) | | | [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | ## Variables | Variable | Description | | --- | --- | -| [addSearchStrategy](./kibana-plugin-plugins-data-public.addsearchstrategy.md) | | | [baseFormattersPublic](./kibana-plugin-plugins-data-public.baseformatterspublic.md) | | | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | -| [defaultSearchStrategy](./kibana-plugin-plugins-data-public.defaultsearchstrategy.md) | | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-public.esquery.md) | | -| [esSearchStrategyProvider](./kibana-plugin-plugins-data-public.essearchstrategyprovider.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [FilterBar](./kibana-plugin-plugins-data-public.filterbar.md) | | | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | -| [hasSearchStategyForIndexPattern](./kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md) | | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | | | [SearchBar](./kibana-plugin-plugins-data-public.searchbar.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.description.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.description.md deleted file mode 100644 index 859935480357c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.description.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [description](./kibana-plugin-plugins-data-public.savedqueryattributes.description.md) - -## SavedQueryAttributes.description property - -Signature: - -```typescript -description: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.filters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.filters.md deleted file mode 100644 index c2c1ac681802b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.filters.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [filters](./kibana-plugin-plugins-data-public.savedqueryattributes.filters.md) - -## SavedQueryAttributes.filters property - -Signature: - -```typescript -filters?: Filter[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.md deleted file mode 100644 index 612be6a1dabc6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) - -## SavedQueryAttributes interface - -Signature: - -```typescript -export interface SavedQueryAttributes -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [description](./kibana-plugin-plugins-data-public.savedqueryattributes.description.md) | string | | -| [filters](./kibana-plugin-plugins-data-public.savedqueryattributes.filters.md) | Filter[] | | -| [query](./kibana-plugin-plugins-data-public.savedqueryattributes.query.md) | Query | | -| [timefilter](./kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md) | SavedQueryTimeFilter | | -| [title](./kibana-plugin-plugins-data-public.savedqueryattributes.title.md) | string | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.query.md deleted file mode 100644 index 96673fc3a8fde..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.query.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [query](./kibana-plugin-plugins-data-public.savedqueryattributes.query.md) - -## SavedQueryAttributes.query property - -Signature: - -```typescript -query: Query; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md deleted file mode 100644 index b4edb059a3dfd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [timefilter](./kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md) - -## SavedQueryAttributes.timefilter property - -Signature: - -```typescript -timefilter?: SavedQueryTimeFilter; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.title.md deleted file mode 100644 index 99ae1b83e8834..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.title.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [title](./kibana-plugin-plugins-data-public.savedqueryattributes.title.md) - -## SavedQueryAttributes.title property - -Signature: - -```typescript -title: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.history.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.history.md deleted file mode 100644 index b2ef4a92c5fef..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.history.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) > [history](./kibana-plugin-plugins-data-public.timefiltersetup.history.md) - -## TimefilterSetup.history property - -Signature: - -```typescript -history: TimeHistoryContract; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.md deleted file mode 100644 index 3375b415e923b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) - -## TimefilterSetup interface - - -Signature: - -```typescript -export interface TimefilterSetup -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [history](./kibana-plugin-plugins-data-public.timefiltersetup.history.md) | TimeHistoryContract | | -| [timefilter](./kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md) | TimefilterContract | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md deleted file mode 100644 index 897ace53a282d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) > [timefilter](./kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md) - -## TimefilterSetup.timefilter property - -Signature: - -```typescript -timefilter: TimefilterContract; -``` diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index bbffb2187f0cf..355477286d445 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -1,20 +1,19 @@ [[tutorial-discovering]] === Discover your data -Using *Discover*, you can enter +Using *Discover*, enter an {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch query] to search your data and filter the results. . Open *Discover*. + -The current index pattern appears below the filter bar, in this case `shakes*`. -You might need to click *New* in the menu bar to refresh the data. +The `shakes*` index pattern appears. -. Click the caret to the right of the current index pattern, and select `ba*`. +. To make `ba*` the current index, click the index pattern dropdown, then select `ba*`. + By default, all fields are shown for each matching document. -. In the search field, enter the following string: +. In the search field, enter: + [source,text] account_number<100 AND balance>47500 @@ -25,11 +24,10 @@ excess of 47,500. Results appear for account numbers 8, 32, 78, 85, and 97. [role="screenshot"] image::images/tutorial-discover-2.png[] + -. To choose which -fields to display, hover the pointer over the list of *Available fields* -and then click *add* next to each field you want include as a column in the table. +. Hover over the list of *Available fields*, then +click *add* next to each field you want include as a column in the table. + -For example, if you add the `account_number` field, the display changes to a list of five +For example, when you add the `account_number` field, the display changes to a list of five account numbers. + [role="screenshot"] diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index b54f4fe5194ad..1a1bcec10ab50 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -1,8 +1,8 @@ [[managing-fields]] -== Index Patterns and Fields +== Index patterns and fields The *Index patterns* UI helps you create and manage -the index patterns that retrieve your data from Elasticsearch. +the index patterns that retrieve your data from {es}. [role="screenshot"] image::images/management-index-patterns.png[] @@ -10,8 +10,8 @@ image::images/management-index-patterns.png[] [float] === Create an index pattern -An index pattern is the glue that connects Kibana to your Elasticsearch data. Create an -index pattern whenever you load your own data into Kibana. To get started, +An index pattern is the glue that connects {kib} to your {es} data. Create an +index pattern whenever you load your own data into {kib}. To get started, click *Create index pattern*, and then follow the guided steps. Refer to <> for the types of index patterns that you can create. @@ -33,7 +33,7 @@ you create is automatically designated as the default pattern. The default index pattern is loaded when you open *Discover*. * *Refresh the index fields list.* You can refresh the index fields list to -pick up any newly-added fields. Doing so also resets Kibana’s popularity counters +pick up any newly-added fields. Doing so also resets the {kib} popularity counters for the fields. The popularity counters are used in *Discover* to sort fields in lists. * [[delete-pattern]]*Delete the index pattern.* This action removes the pattern from the list of @@ -60,7 +60,7 @@ Kibana has field formatters for the following field types: * <> [[field-formatters-string]] -=== String Field Formatters +=== String field formatters String fields support the `String` and `Url` formatters. @@ -69,7 +69,7 @@ include::field-formatters/string-formatter.asciidoc[] include::field-formatters/url-formatter.asciidoc[] [[field-formatters-date]] -=== Date Field Formatters +=== Date field formatters Date fields support the `Date`, `Url`, and `String` formatters. @@ -81,19 +81,19 @@ include::field-formatters/string-formatter.asciidoc[] include::field-formatters/url-formatter.asciidoc[] [[field-formatters-geopoint]] -=== Geographic Point Field Formatters +=== Geographic point field formatters Geographic point fields support the `String` formatter. include::field-formatters/string-formatter.asciidoc[] [[field-formatters-numeric]] -=== Numeric Field Formatters +=== Numeric field formatters Numeric fields support the `Url`, `Bytes`, `Duration`, `Number`, `Percentage`, `String`, and `Color` formatters. The `Bytes`, `Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using -the <> syntax that Kibana maintains. +the <> syntax that {kib} maintains. include::field-formatters/url-formatter.asciidoc[] @@ -104,25 +104,22 @@ include::field-formatters/duration-formatter.asciidoc[] include::field-formatters/color-formatter.asciidoc[] [[scripted-fields]] -=== Scripted Fields +=== Scripted fields -Scripted fields compute data on the fly from the data in your Elasticsearch indices. Scripted field data is shown on -the Discover tab as part of the document data, and you can use scripted fields in your visualizations. -Scripted field values are computed at query time so they aren't indexed and cannot be searched using Kibana's default -query language. However they can be queried using Kibana's new <>. Scripted -fields are also supported in the filter bar. +Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on +the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default +query language. WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on -Kibana's performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are +{kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. -When you define a scripted field in Kibana, you have a choice of scripting languages. Starting with 5.0, the default +When you define a scripted field in {kib}, you have a choice of scripting languages. In 5.0 and later, the default options are {ref}/modules-scripting-expression.html[Lucene expressions] and {ref}/modules-scripting-painless.html[Painless]. -While you can use other scripting languages if you enable dynamic scripting for them in Elasticsearch, this is not recommended +While you can use other scripting languages if you enable dynamic scripting for them in {es}, this is not recommended because they cannot be sufficiently {ref}/modules-scripting-security.html[sandboxed]. -WARNING: Use of Groovy, JavaScript, and Python scripting is deprecated starting in Elasticsearch 5.0, and support for those -scripting languages will be removed in the future. +WARNING: In 5.0 and later, Groovy, JavaScript, and Python scripting are deprecated and unsupported. You can reference any single value numeric field in your expressions, for example: @@ -130,44 +127,40 @@ You can reference any single value numeric field in your expressions, for exampl doc['field_name'].value ---- -For more background on scripted fields and additional examples, refer to this blog: -https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in Kibana scripted fields] +For more information on scripted fields and additional examples, refer to +https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] [float] [[create-scripted-field]] -=== Creating a Scripted Field -To create a scripted field: +=== Create a scripted field -. Go to *Management > Kibana > Index Patterns* +. Go to *Management > {kib} > Index Patterns* . Select the index pattern you want to add a scripted field to. -. Go to the pattern's *Scripted fields* tab. -. Click *Add scripted field*. +. Go to the *Scripted fields* tab for the index pattern, then click *Add scripted field*. . Enter a name for the scripted field. . Enter the expression that you want to use to compute a value on the fly from your index data. . Click *Create field*. -For more information about scripted fields in Elasticsearch, see +For more information about scripted fields in {es}, see {ref}/modules-scripting.html[Scripting]. [float] [[update-scripted-field]] -=== Updating a Scripted Field -To modify a scripted field: +=== Update a scripted field -. Go to *Management > Kibana > Index Patterns* -. Click the index pattern's *Scripted fields* tab. +. Go to *Management > {kib} > Index Patterns* +. Click the *Scripted fields* tab for the index pattern. . Click the *Edit* button for the scripted field you want to change. -. Make your changes and then click *Save field* to update the field. +. Make your changes, then click *Save field*. -WARNING: Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get +WARNING: Built-in validation is unsupported for scripted fields. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. [float] [[delete-scripted-field]] -=== Deleting a Scripted Field -To delete a scripted field: +=== Delete a scripted field -. Go to *Management > Kibana > Index Patterns* -. Click the index pattern's *Scripted fields* tab. -. Click the *Delete* button for the scripted field you want to remove. -. Click *Delete* in the confirmation window. +. Go to *Management > {kib} > Index Patterns* +. Click the *Scripted fields* tab for the index pattern. +. Click *Delete* for the scripted field you want to remove. +. Click *Delete* on the confirmation window. diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 44610a2fd3426..205e614dc21cd 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -2,13 +2,13 @@ [[watcher-ui]] == Watcher -Watcher is an {es} feature that you can use to create actions based on -conditions, which are periodically evaluated using queries on your data. -Watches are helpful for analyzing mission-critical and business-critical -streaming data. For example, you might watch application logs for performance +Watcher is an {es} feature that you can use to create actions based on +conditions, which are periodically evaluated using queries on your data. +Watches are helpful for analyzing mission-critical and business-critical +streaming data. For example, you might watch application logs for performance outages or audit access logs for security threats. -To get started with the Watcher UI, go to *Management > Elasticsearch > Watcher*. +To get started with the Watcher UI, go to *Management > Elasticsearch > Watcher*. With this UI, you can: * <> @@ -20,10 +20,10 @@ With this UI, you can: image:management/watcher-ui/images/watches.png["Watcher list"] {ref}/xpack-alerting.html[Alerting on cluster and index events] -is a good source for detailed -information on how watches work. If you are using the UI to create a -threshold watch, take a look at the different watcher actions. If you are -creating an advanced watch, you should be familiar with the parts of a +is a good source for detailed +information on how watches work. If you are using the UI to create a +threshold watch, take a look at the different watcher actions. If you are +creating an advanced watch, you should be familiar with the parts of a watch—input, schedule, condition, and actions. [float] @@ -40,41 +40,40 @@ and either of these watcher roles: * `watcher_admin`. You can perform all Watcher actions, including create and edit watches. * `watcher_user`. You can view watches, but not create or edit them. -You can manage roles in *Management > Security > Roles*, or use the -<>. Watches are shared between -all users with the same role. +You can manage roles in *Management > Security > Roles*, or use the +<>. Watches are shared between +all users with the same role. -NOTE: If you are creating a threshold watch, you must also have index management -privileges. See +NOTE: If you are creating a threshold watch, you must also have the `view_index_metadata` index privilege. See <> for detailed information. [float] [[watcher-create-threshold-alert]] === Create a threshold alert -A threshold alert is one of the most common types of watches that you can create. -This alert periodically checks when your data is above, below, equals, +A threshold alert is one of the most common types of watches that you can create. +This alert periodically checks when your data is above, below, equals, or is in between a certain threshold within a given time interval. -The following example walks you through creating a threshold alert. The alert -is triggered when the maximum total CPU usage on a machine goes above a -certain percentage. The example uses https://www.elastic.co/products/beats/metricbeat[Metricbeat] -to collect metrics from your systems and services. -{metricbeat-ref}/metricbeat-installation.html[Learn more] on how to install +The following example walks you through creating a threshold alert. The alert +is triggered when the maximum total CPU usage on a machine goes above a +certain percentage. The example uses https://www.elastic.co/products/beats/metricbeat[Metricbeat] +to collect metrics from your systems and services. +{metricbeat-ref}/metricbeat-installation.html[Learn more] on how to install and get started with Metricbeat. [float] ==== Define the watch input and schedule -. Click *Create* and then select *Create threshold alert*. +. Click *Create* and then select *Create threshold alert*. + You're navigated to a page where you're asked to define the watch name, the data that you want to evaluate, and how often you want to trigger the watch. . Enter a name that you want to call the alert, for example, `cpu_threshold_alert`. -. In the *Indices to query* field, enter `metricbeat-*` and select `@timestamp` -as the time field. +. In the *Indices to query* field, enter `metricbeat-*` and select `@timestamp` +as the time field. . Use the default schedule to run the watch every 1 minute. + @@ -84,22 +83,22 @@ image:management/watcher-ui/images/threshold-alert/create-threshold-alert-create [float] ==== Add a condition -You should now see a panel with default conditions and a visualization of the -data based on those conditions. The condition evaluates the data you’ve loaded +You should now see a panel with default conditions and a visualization of the +data based on those conditions. The condition evaluates the data you’ve loaded into the watch and determines if any action is required. -. Click the `WHEN` expression and change the value to `max()`. +. Click the `WHEN` expression and change the value to `max()`. + -The `OF` expression now appears. +The `OF` expression now appears. -. Search for `system.process.cpu.total.norm.pct` and select it from the list. +. Search for `system.process.cpu.total.norm.pct` and select it from the list. -. Select the `IS ABOVE` expression and change the value to `.25` to trigger +. Select the `IS ABOVE` expression and change the value to `.25` to trigger an alert whenever the CPU is above 25%. + -As you change the condition, the visualization is automatically updated. The black -line represents the threshold (25%), while the green fluctuating line +As you change the condition, the visualization is automatically updated. The black +line represents the threshold (25%), while the green fluctuating line represents the change in CPU over the set time period. + [role="screenshot"] @@ -108,46 +107,46 @@ image:management/watcher-ui/images/threshold-alert/threshold-alert-condition.png [float] ==== Add an action -Now that the condition is set, you must add an action. The action triggers -when the watch condition is met. For a complete list of actions and how to configure them, see +Now that the condition is set, you must add an action. The action triggers +when the watch condition is met. For a complete list of actions and how to configure them, see {ref}/action-conditions.html[Adding conditions to actions]. In this example, you’ll configure an email action. You must have an {ref}/actions-email.html#configuring-email[email account configured] -in {es} for this example to work. +in {es} for this example to work. . Click *Add action* and select *Email*. -. In the *To email address* field, enter one or more email addresses to whom -you want to send the message when the condition is met. +. In the *To email address* field, enter one or more email addresses to whom +you want to send the message when the condition is met. . Enter a subject and body for the email. + [role="screenshot"] image:management/watcher-ui/images/threshold-alert/threshold-alert-action.png["Action for threshold alert"] -. To test the action before saving the watch, click *Send test email*. +. To test the action before saving the watch, click *Send test email*. + A sample email is sent using the configuration you set up. -. Click *Create alert*. +. Click *Create alert*. + -The alert appears on the Watcher overview page, where you can drill down into +The alert appears on the Watcher overview page, where you can drill down into the watch history and status. [float] ==== Delete the alert -In this example, you set the threshold to 25% so you can see the watch fire. In -a real-world scenario, this threshold is likely too low because the alerts are -so frequent. Once you are done experimenting, you should delete the alert. +In this example, you set the threshold to 25% so you can see the watch fire. In +a real-world scenario, this threshold is likely too low because the alerts are +so frequent. Once you are done experimenting, you should delete the alert. Find the alert on the Watcher overview page and click the trash icon in the *Actions* column. [float] ==== Edit the alert -Alternatively, you can keep the alert and adjust the threshold value. To edit -an alert, find the alert on the Watcher overview page and click the pencil icon -in the *Actions* column. +Alternatively, you can keep the alert and adjust the threshold value. To edit +an alert, find the alert on the Watcher overview page and click the pencil icon +in the *Actions* column. [float] [[watcher-getting-started]] @@ -161,13 +160,13 @@ last fired, and last triggered. A watch has one of four states: * *Disabled.* The watch will not fire under any circumstances. From this page you can drill down into a watch to investigate its history -and status. +and status. [float] ==== View watch history -The *Execution history* tab shows each time the watch is triggered and the -results of the query, whether the condition was met, and what actions were taken. +The *Execution history* tab shows each time the watch is triggered and the +results of the query, whether the condition was met, and what actions were taken. [role="screenshot"] image:management/watcher-ui/images/execution-history.png["Execution history tab"] @@ -175,10 +174,10 @@ image:management/watcher-ui/images/execution-history.png["Execution history tab" [float] ==== Acknowledge action status -The *Action statuses* tab lists all actions associated with the watch and -the state of each action. If the action is firing, you can acknowledge the -watch to prevent too many executions of the same action for the same watch. -See {ref}/actions.html#actions-ack-throttle[Acknowledgement and throttling] for details. +The *Action statuses* tab lists all actions associated with the watch and +the state of each action. If the action is firing, you can acknowledge the +watch to prevent too many executions of the same action for the same watch. +See {ref}/actions.html#actions-ack-throttle[Acknowledgement and throttling] for details. [role="screenshot"] image:management/watcher-ui/images/alerts-status.png["Action status tab"] @@ -189,28 +188,28 @@ image:management/watcher-ui/images/alerts-status.png["Action status tab"] Actions for deactivating and deleting a watch are on each watch detail page: -* *Deactivate a watch* if you know a situation is planned that will -cause a false alarm. You can reactivate the watch when the situation is resolved. -* *Delete a watch* to permanently remove it from the system. You can delete -the watch you are currently viewing, or go to the Watcher overview, and -delete watches in bulk. +* *Deactivate a watch* if you know a situation is planned that will +cause a false alarm. You can reactivate the watch when the situation is resolved. +* *Delete a watch* to permanently remove it from the system. You can delete +the watch you are currently viewing, or go to the Watcher overview, and +delete watches in bulk. [float] [[watcher-create-advanced-watch]] === Create an advanced watch -Advanced watches are for users who are more familiar with {es} query syntax and -the Watcher framework. The UI is aligned with using the REST APIs. +Advanced watches are for users who are more familiar with {es} query syntax and +the Watcher framework. The UI is aligned with using the REST APIs. For more information, see {ref}/query-dsl.html[Query DSL]. [float] ==== Create the watch -On the Watch overview page, click *Create* and choose *Create advanced watch*. -An advanced watch requires a name and ID. Name is a user-friendly way to -identify the watch, and ID refers to the identifier used by {es}. Refer to -{ref}/how-watcher-works.html#watch-definition[Watch definition] for how -to input the watch JSON. +On the Watch overview page, click *Create* and choose *Create advanced watch*. +An advanced watch requires a name and ID. Name is a user-friendly way to +identify the watch, and ID refers to the identifier used by {es}. Refer to +{ref}/how-watcher-works.html#watch-definition[Watch definition] for how +to input the watch JSON. [role="screenshot"] image:management/watcher-ui/images/advanced-watch/advanced-watch-create.png["Create advanced watch"] @@ -218,7 +217,7 @@ image:management/watcher-ui/images/advanced-watch/advanced-watch-create.png["Cre [float] ==== Simulate the watch -The *Simulate* tab allows you to override parts of the watch, and then run a +The *Simulate* tab allows you to override parts of the watch, and then run a simulation. Be aware of these implementation details on overrides: * Trigger overrides use {ref}/common-options.html#date-math[date math]. @@ -226,7 +225,7 @@ simulation. Be aware of these implementation details on overrides: * Condition overrides indicates if you want to force the condition to always be `true`. * Action overrides support {ref}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode[multiple options]. -After starting the simulation, you’ll see a results screen. For more information +After starting the simulation, you’ll see a results screen. For more information on the fields in the response, see the {ref}/watcher-api-execute-watch.html[Execute watch API]. [role="screenshot"] @@ -235,7 +234,7 @@ image:management/watcher-ui/images/advanced-watch/advanced-watch-simulate.png["C [float] ==== Examples of advanced watches -Refer to these examples for creating an advanced watch: +Refer to these examples for creating an advanced watch: * {ref}/watch-cluster-status.html[Watch the status of an {es} cluster] * {ref}/watching-meetup-data.html[Watch event data] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 8ad5330f3fda5..fd835bde83322 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -64,4 +64,9 @@ This page has moved. Please see <>. [role="exclude",id="tilemap"] == Coordinate map -This page has moved. Please see <>. +This page has moved. Please see <>. + +[role="exclude",id="visualize-maps"] +== Maps + +This page has moved. Please see <>. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 80d04c260e25f..71bb7b81ea420 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -220,15 +220,13 @@ requests. Supported on Elastic Cloud Enterprise. `map.includeElasticMapsService:`:: *Default: true* Set to false to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` -and the tile layer configured by `map.tilemap.url` will be available in the <>, -<>, and <>. +and the tile layer configured by `map.tilemap.url` will be available in <>. `map.proxyElasticMapsServiceInMaps:`:: *Default: false* Set to true to proxy all <> Elastic Maps Service requests through the Kibana server. -This setting does not impact <> and <>. [[regionmap-settings]] `map.regionmap:`:: Specifies additional vector layers for -use in <> visualizations. Supported on {ece}. Each layer +use in <> visualizations. Supported on {ece}. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index f6be2040e3e8c..ebc2f404d43c1 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -40,11 +40,7 @@ data sets. <>:: * *<>* — Displays geospatial data in {kib}. -* *Coordinate map* — Displays points on a map using a geohash aggregation. - -* *Region map* — Merges any structured map data onto a shape. - -* *Heat map* — Displays shaded cells within a matrix. +* <>:: Display shaded cells within a matrix. <>:: diff --git a/docs/visualize/aggregations.asciidoc b/docs/visualize/aggregations.asciidoc index 95aa586e6ba18..868e66d0f4e36 100644 --- a/docs/visualize/aggregations.asciidoc +++ b/docs/visualize/aggregations.asciidoc @@ -58,8 +58,6 @@ You can also nest these aggregations. For example, if you want to produce a thir {ref}/search-aggregations-pipeline-serialdiff-aggregation.html[Serial diff]:: Values in a time series are subtracted from itself at different time lags or periods. -Custom {kib} plugins can <>, which includes support for adding more aggregations. - [float] [[visualize-sibling-pipeline-aggregations]] === Sibling pipeline aggregations diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 349fa681a9777..51342847080e0 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -1,116 +1,14 @@ -[[visualize-maps]] -== Maps - -To tell a story and answer questions about your geographical data, you can create several types of interactive maps with Visualize. - -Visualize supports the following maps: - -* *Coordinate* — Display latitude and longitude coordinates that are associated to the specified bucket aggregation. - -* *Region* — Display colored boundary vector shapes using a gradient. Darker colors indicate larger values, and lighter colors indicate smaller values. - -* *Heat* — Display graphical representations of data where the individual values are represented by colors. - -NOTE: The maps in Visualize have been replaced with <>, which offers more functionality. - -[float] -[[coordinate-map]] -=== Coordinate map - -Use a coordinate map when your data set includes latitude and longitude values. For example, use a coordinate map to see the varying popularity of destination airports using the sample flight data. - -[role="screenshot"] -image::images/visualize_coordinate_map_example.png[] - -[float] -[[build-coordinate-map]] -==== Build a coordinate map - -Configure the `kibana.yml` settings and add the aggregations. - -. Configure the following `kibana.yml` settings: - -* Set `xpack.maps.showMapVisualizationTypes` to `true`. - -* To use a tile service provider for coordinate maps other than https://www.elastic.co/elastic-maps-service[Elastic Maps Service], configure the <>. - -. To display your data on the coordinate map, use the following aggregations: - -* <> - -* <> - -. Specify the geohash bucket aggregation options: - -* *Precision* slider — Determines the granularity of the results displayed on the map. To show the *Precision* slider, deselect *Change precision on map zoom*. For information on the area specified by each precision level, refer to {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid]. -+ -NOTE: Higher precisions increase memory usage for the browser that displays {kib} and the underlying -{es} cluster. - -* *Place markers off grid (use {ref}/search-aggregations-metrics-geocentroid-aggregation.html[geocentroid])* — When you selected, the markers are -placed in the center of all documents in the bucket, and a more accurate visualization is created. When deselected, the markers are placed in the center -of the geohash grid cell. -+ -NOTE: When you have multiple values in the geo_point, the coordinate map is unable to accurately calculate the geo_centroid. - -[float] -[[navigate-coordinate-map]] -==== Navigate the coordinate map - -To navigate the coordinate map, use the navigation options. - -* To move the map center, click and hold anywhere on the map and move the cursor. - -* To change the zoom level, click *Zoom In* or *Zoom out* image:images/viz-zoom.png[]. - -* To automatically crop the map boundaries to the -geohash buckets that have at least one result, click *Fit Data Bounds* image:images/viz-fit-bounds.png[]. - -[float] -[[region-map]] -=== Region map - -Use region maps when you want to show statistical data on a geographic area, such as a county, country, province, or state. For example, use a region map if you want to see the average sales for each country with the sample eCommerce order data. - -[role="screenshot"] -image::images/visualize_region_map_example.png[] - -[float] -[[build-region-maps]] -==== Build a region map - -Configure the `kibana.yml` settings and add the aggregations. - -. In `kibana.yml`, set `xpack.maps.showMapVisualizationTypes` to `true`. - -. To display your data on the region map, use the following aggregations: - -* <> -* <> -* <> - -[float] -[[navigate-region-map]] -==== Navigate the region map - -To navigate the region map, use the navigation options. - -* To change the zoom level, click *Zoom In* or *Zoom out* image:images/viz-zoom.png[]. - -* To automatically crop the map boundaries, click *Fit Data Bounds* image:images/viz-fit-bounds.png[]. - -[float] [[heat-map]] -=== Heat map +== Heat map -Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. +Display graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. [role="screenshot"] image::images/visualize_heat_map_example.png[] [float] [[build-heat-map]] -==== Build a heat map +=== Build a heat map To display your data on the heat map, use the supported aggregations. @@ -123,7 +21,7 @@ Heat maps support the following aggregations: [float] [[navigate-heatmap]] -==== Change the color ranges +=== Change the color ranges When only one color displays on the heat map, you might need to change the color ranges. diff --git a/package.json b/package.json index 7c82bf8d6f881..a97c21164d406 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "typespec": "typings-tester --config x-pack/legacy/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/legacy/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", - "start": "node --trace-warnings --throw-deprecation scripts/kibana --dev", + "start": "node scripts/kibana --dev", "debug": "node --nolazy --inspect scripts/kibana --dev", "debug-break": "node --nolazy --inspect-brk scripts/kibana --dev", "lint": "yarn run lint:es && yarn run lint:sass", @@ -203,7 +203,7 @@ "leaflet-responsive-popup": "0.6.4", "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", - "less": "^3.0.2", + "less": "^2.7.3", "less-loader": "5.0.0", "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.clonedeep": "^4.5.0", diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index dc3fa38f3129c..05840926d35de 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -14,7 +14,7 @@ cacheDir="$HOME/.kibana" RED='\033[0;31m' C_RESET='\033[0m' # Reset color -export NODE_OPTIONS="$NODE_OPTIONS --throw-deprecation --max-old-space-size=4096" +export NODE_OPTIONS="$NODE_OPTIONS --max-old-space-size=4096" ### ### Since the Jenkins logging output collector doesn't look like a TTY @@ -168,4 +168,4 @@ if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then export JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA fi -export CI_ENV_SETUP=true \ No newline at end of file +export CI_ENV_SETUP=true diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts deleted file mode 100644 index 08d5955d3fae9..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -import { FilterStateManager } from './filter_state_manager'; - -import { StubState } from './test_helpers/stub_state'; -import { getFilter } from './test_helpers/get_stub_filter'; -import { FilterManager, esFilters } from '../../../../../../plugins/data/public'; - -import { coreMock } from '../../../../../../core/public/mocks'; -const setupMock = coreMock.createSetup(); - -setupMock.uiSettings.get.mockImplementation((key: string) => { - return true; -}); - -describe('filter_state_manager', () => { - let appStateStub: StubState; - let globalStateStub: StubState; - - let filterManager: FilterManager; - - beforeEach(() => { - appStateStub = new StubState(); - globalStateStub = new StubState(); - filterManager = new FilterManager(setupMock.uiSettings); - }); - - describe('app_state_undefined', () => { - beforeEach(() => { - // FilterStateManager is tested indirectly. - // Therefore, we don't need it's instance. - new FilterStateManager( - globalStateStub, - () => { - return undefined; - }, - filterManager - ); - }); - - test('should NOT watch state until both app and global state are defined', done => { - const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - globalStateStub.filters.push(f1); - - setTimeout(() => { - expect(filterManager.getGlobalFilters()).toHaveLength(0); - done(); - }, 100); - }); - - test('should NOT update app URL when filter manager filters are set', async () => { - appStateStub.save = sinon.stub(); - globalStateStub.save = sinon.stub(); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - const f2 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - - filterManager.setFilters([f1, f2]); - - sinon.assert.notCalled(appStateStub.save); - sinon.assert.calledOnce(globalStateStub.save); - }); - }); - - describe('app_state_defined', () => { - let filterStateManager: FilterStateManager; - beforeEach(() => { - // FilterStateManager is tested indirectly. - // Therefore, we don't need it's instance. - filterStateManager = new FilterStateManager( - globalStateStub, - () => { - return appStateStub; - }, - filterManager - ); - }); - - afterEach(() => { - filterStateManager.destroy(); - }); - - test('should update filter manager global filters', done => { - const updateSubscription = filterManager.getUpdates$().subscribe(() => { - expect(filterManager.getGlobalFilters()).toHaveLength(1); - if (updateSubscription) { - updateSubscription.unsubscribe(); - } - done(); - }); - - const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, true, true, 'age', 34); - globalStateStub.filters.push(f1); - }); - - test('should update filter manager app filter', done => { - const updateSubscription = filterManager.getUpdates$().subscribe(() => { - expect(filterManager.getAppFilters()).toHaveLength(1); - if (updateSubscription) { - updateSubscription.unsubscribe(); - } - done(); - }); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - appStateStub.filters.push(f1); - }); - - test('should update URL when filter manager filters are set', () => { - appStateStub.save = sinon.stub(); - globalStateStub.save = sinon.stub(); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - const f2 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - - filterManager.setFilters([f1, f2]); - - sinon.assert.calledOnce(appStateStub.save); - sinon.assert.calledOnce(globalStateStub.save); - }); - - test('should update URL when filter manager filters are added', () => { - appStateStub.save = sinon.stub(); - globalStateStub.save = sinon.stub(); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - const f2 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - - filterManager.addFilters([f1, f2]); - - sinon.assert.calledOnce(appStateStub.save); - sinon.assert.calledOnce(globalStateStub.save); - }); - }); - - describe('bug fixes', () => { - /* - ** This test is here to reproduce a bug where a filter manager update - ** would cause filter state manager detects those changes - ** And triggers *another* filter manager update. - */ - test('should NOT re-trigger filter manager', done => { - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - filterManager.setFilters([f1]); - const setFiltersSpy = sinon.spy(filterManager, 'setFilters'); - - f1.meta.negate = true; - filterManager.setFilters([f1]); - - setTimeout(() => { - expect(setFiltersSpy.callCount).toEqual(1); - done(); - }, 100); - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts deleted file mode 100644 index e095493c94c58..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { Subscription } from 'rxjs'; -import { State } from 'ui/state_management/state'; -import { FilterManager, esFilters, Filter } from '../../../../../../plugins/data/public'; - -type GetAppStateFunc = () => { filters?: Filter[]; save?: () => void } | undefined | null; - -/** - * FilterStateManager is responsible for watching for filter changes - * and syncing with FilterManager, as well as syncing FilterManager changes - * back to the URL. - **/ -export class FilterStateManager { - private filterManagerUpdatesSubscription: Subscription; - - filterManager: FilterManager; - globalState: State; - getAppState: GetAppStateFunc; - interval: number | undefined; - - constructor(globalState: State, getAppState: GetAppStateFunc, filterManager: FilterManager) { - this.getAppState = getAppState; - this.globalState = globalState; - this.filterManager = filterManager; - - this.watchFilterState(); - - this.filterManagerUpdatesSubscription = this.filterManager.getUpdates$().subscribe(() => { - this.updateAppState(); - }); - } - - destroy() { - if (this.interval) { - clearInterval(this.interval); - } - this.filterManagerUpdatesSubscription.unsubscribe(); - } - - private watchFilterState() { - // This is a temporary solution to remove rootscope. - // Moving forward, state should provide observable subscriptions. - this.interval = window.setInterval(() => { - const appState = this.getAppState(); - const stateUndefined = !appState || !this.globalState; - if (stateUndefined) return; - - const globalFilters = this.globalState.filters || []; - const appFilters = (appState && appState.filters) || []; - - const globalFilterChanged = !esFilters.compareFilters( - this.filterManager.getGlobalFilters(), - globalFilters, - esFilters.COMPARE_ALL_OPTIONS - ); - const appFilterChanged = !esFilters.compareFilters( - this.filterManager.getAppFilters(), - appFilters, - esFilters.COMPARE_ALL_OPTIONS - ); - const filterStateChanged = globalFilterChanged || appFilterChanged; - - if (!filterStateChanged) return; - - const newGlobalFilters = _.cloneDeep(globalFilters); - const newAppFilters = _.cloneDeep(appFilters); - FilterManager.setFiltersStore(newAppFilters, esFilters.FilterStateStore.APP_STATE); - FilterManager.setFiltersStore(newGlobalFilters, esFilters.FilterStateStore.GLOBAL_STATE); - - this.filterManager.setFilters(newGlobalFilters.concat(newAppFilters)); - }, 10); - } - - private saveState() { - const appState = this.getAppState(); - if (appState && appState.save) appState.save(); - this.globalState.save(); - } - - private updateAppState() { - // Update Angular state before saving State objects (which save it to URL) - const partitionedFilters = this.filterManager.getPartitionedFilters(); - const appState = this.getAppState(); - if (appState) { - appState.filters = partitionedFilters.appFilters; - } - this.globalState.filters = partitionedFilters.globalFilters; - this.saveState(); - } -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts deleted file mode 100644 index ebb622783c3d1..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { FilterStateManager } from './filter_state_manager'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts deleted file mode 100644 index 74eaad34fe160..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Filter } from '../../../../../../../plugins/data/public'; - -export function getFilter( - store: any, // I don't want to export only for this, as it should move to data plugin - disabled: boolean, - negated: boolean, - queryKey: string, - queryValue: any -): Filter { - return { - $state: { - store, - }, - meta: { - index: 'logstash-*', - disabled, - negate: negated, - alias: null, - }, - query: { - match: { - [queryKey]: queryValue, - }, - }, - }; -} diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 9187e207ed0d6..61d8621a36843 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -28,11 +28,6 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart } from './plugin'; -export { - SavedQueryAttributes, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../plugins/data/public'; export { // agg_types AggParam, // only the type is used externally, only in vis editor @@ -50,7 +45,6 @@ export { /** @public static code */ export * from '../common'; -export { FilterStateManager } from './filter/filter_manager'; export { // agg_types TODO need to group these under a namespace or prefix AggConfigs, diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 18230646ab412..f40cda8760bc7 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -18,12 +18,7 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { - DataPublicPluginStart, - addSearchStrategy, - defaultSearchStrategy, - DataPublicPluginSetup, -} from '../../../../plugins/data/public'; +import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../plugins/data/public'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { @@ -111,9 +106,6 @@ export class DataPlugin public setup(core: CoreSetup, { data, uiActions }: DataPluginSetupDependencies) { setInjectedMetadata(core.injectedMetadata); - // This is to be deprecated once we switch to the new search service fully - addSearchStrategy(defaultSearchStrategy); - uiActions.attachAction( SELECT_RANGE_TRIGGER, selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts index f792796230757..f238a2287ecdb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PhraseFilter, IndexPattern, TimefilterSetup } from '../../../../../plugins/data/public'; +import { PhraseFilter, IndexPattern, TimefilterContract } from '../../../../../plugins/data/public'; import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; export function createSearchSource( @@ -27,7 +27,7 @@ export function createSearchSource( aggs: any, useTimeFilter: boolean, filters: PhraseFilter[] = [], - timefilter: TimefilterSetup['timefilter'] + timefilter: TimefilterContract ) { const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); // Do not not inherit from rootSearchSource to avoid picking up time and globals diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts index 56b42f295ce15..8364c82efecdb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts @@ -26,7 +26,7 @@ import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterSetup } from '../../../../../plugins/data/public'; +import { IFieldType, TimefilterContract } from '../../../../../plugins/data/public'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators @@ -74,7 +74,7 @@ const termsAgg = ({ field, size, direction, query }: TermsAggArgs) => { export class ListControl extends Control { private getInjectedVar: InputControlVisDependencies['core']['injectedMetadata']['getInjectedVar']; - private timefilter: TimefilterSetup['timefilter']; + private timefilter: TimefilterContract; abortController?: AbortController; lastAncestorValues: any; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts index b9191436b5968..d9b43c9dff201 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts @@ -26,7 +26,7 @@ import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterSetup } from '../.../../../../../../plugins/data/public'; +import { IFieldType, TimefilterContract } from '../.../../../../../../plugins/data/public'; const minMaxAgg = (field?: IFieldType) => { const aggBody: any = {}; @@ -52,7 +52,7 @@ const minMaxAgg = (field?: IFieldType) => { }; export class RangeControl extends Control { - timefilter: TimefilterSetup['timefilter']; + timefilter: TimefilterContract; abortController: any; min: any; max: any; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js deleted file mode 100644 index 87eb283639c78..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { createStateStub } from './_utils'; -import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; -import { npStart } from 'ui/new_platform'; - -describe('context app', function() { - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices()); - beforeEach(ngMock.module('app/discover')); - - describe('action addFilter', function() { - let addFilter; - - beforeEach( - ngMock.inject(function createPrivateStubs() { - addFilter = getQueryParameterActions().addFilter; - }) - ); - - it('should pass the given arguments to the filterManager', function() { - const state = createStateStub(); - const filterManagerAddStub = npStart.plugins.data.query.filterManager.addFilters; - - addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - - //get the generated filter - const generatedFilter = filterManagerAddStub.firstCall.args[0][0]; - const queryKeys = Object.keys(generatedFilter.query.match_phrase); - expect(filterManagerAddStub.calledOnce).to.be(true); - expect(queryKeys[0]).to.eql('FIELD_NAME'); - expect(generatedFilter.query.match_phrase[queryKeys[0]]).to.eql('FIELD_VALUE'); - }); - - it('should pass the index pattern id to the filterManager', function() { - const state = createStateStub(); - const filterManagerAddStub = npStart.plugins.data.query.filterManager.addFilters; - - addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - - const generatedFilter = filterManagerAddStub.firstCall.args[0][0]; - expect(generatedFilter.meta.index).to.eql('INDEX_PATTERN_ID'); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js deleted file mode 100644 index 9ba425bb0e489..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; -import { createStateStub } from './_utils'; -import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; - -describe('context app', function() { - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices()); - beforeEach(ngMock.module('app/discover')); - - describe('action setPredecessorCount', function() { - let setPredecessorCount; - - beforeEach( - ngMock.inject(function createPrivateStubs() { - setPredecessorCount = getQueryParameterActions().setPredecessorCount; - }) - ); - - it('should set the predecessorCount to the given value', function() { - const state = createStateStub(); - - setPredecessorCount(state)(20); - - expect(state.queryParameters.predecessorCount).to.equal(20); - }); - - it('should limit the predecessorCount to 0 as a lower bound', function() { - const state = createStateStub(); - - setPredecessorCount(state)(-1); - - expect(state.queryParameters.predecessorCount).to.equal(0); - }); - - it('should limit the predecessorCount to 10000 as an upper bound', function() { - const state = createStateStub(); - - setPredecessorCount(state)(20000); - - expect(state.queryParameters.predecessorCount).to.equal(10000); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js deleted file mode 100644 index 39dde2d8bb7cf..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; - -import { createStateStub } from './_utils'; -import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; - -describe('context app', function() { - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices()); - beforeEach(ngMock.module('app/discover')); - - describe('action setQueryParameters', function() { - let setQueryParameters; - - beforeEach( - ngMock.inject(function createPrivateStubs() { - setQueryParameters = getQueryParameterActions().setQueryParameters; - }) - ); - - it('should update the queryParameters with valid properties from the given object', function() { - const state = createStateStub({ - queryParameters: { - additionalParameter: 'ADDITIONAL_PARAMETER', - }, - }); - - setQueryParameters(state)({ - anchorId: 'ANCHOR_ID', - columns: ['column'], - defaultStepSize: 3, - filters: ['filter'], - indexPatternId: 'INDEX_PATTERN', - predecessorCount: 100, - successorCount: 100, - sort: ['field'], - }); - - expect(state.queryParameters).to.eql({ - additionalParameter: 'ADDITIONAL_PARAMETER', - anchorId: 'ANCHOR_ID', - columns: ['column'], - defaultStepSize: 3, - filters: ['filter'], - indexPatternId: 'INDEX_PATTERN', - predecessorCount: 100, - successorCount: 100, - sort: ['field'], - }); - }); - - it('should ignore invalid properties', function() { - const state = createStateStub(); - - setQueryParameters(state)({ - additionalParameter: 'ADDITIONAL_PARAMETER', - }); - - expect(state.queryParameters).to.eql(createStateStub().queryParameters); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js deleted file mode 100644 index c05f5b4aff3bc..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; - -import { createStateStub } from './_utils'; -import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; - -describe('context app', function() { - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices()); - beforeEach(ngMock.module('app/discover')); - - describe('action setSuccessorCount', function() { - let setSuccessorCount; - - beforeEach( - ngMock.inject(function createPrivateStubs() { - setSuccessorCount = getQueryParameterActions().setSuccessorCount; - }) - ); - - it('should set the successorCount to the given value', function() { - const state = createStateStub(); - - setSuccessorCount(state)(20); - - expect(state.queryParameters.successorCount).to.equal(20); - }); - - it('should limit the successorCount to 0 as a lower bound', function() { - const state = createStateStub(); - - setSuccessorCount(state)(-1); - - expect(state.queryParameters.successorCount).to.equal(0); - }); - - it('should limit the successorCount to 10000 as an upper bound', function() { - const state = createStateStub(); - - setSuccessorCount(state)(20000); - - expect(state.queryParameters.successorCount).to.equal(10000); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 6947d985be436..8a8b5d8e0e3ea 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -70,7 +70,6 @@ export { IIndexPattern, IndexPattern, indexPatterns, - hasSearchStategyForIndexPattern, IFieldType, SearchSource, ISearchSource, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/_stubs.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js similarity index 97% rename from src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/_stubs.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js index 53be4e5bd0f2d..f6ed570be2c37 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/_stubs.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import moment from 'moment'; -import { SearchSource } from '../../../../../kibana_services'; +import { SearchSource } from '../../../../../../../../../plugins/data/public'; export function createIndexPatternsStub() { return { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js similarity index 73% rename from src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js index 63834fb750e21..0bc2cbacc1eee 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js @@ -17,28 +17,19 @@ * under the License. */ -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; - import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; -import { fetchAnchorProvider } from '../anchor'; +import { fetchAnchorProvider } from './anchor'; describe('context app', function() { - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(ngMock.module('app/discover')); - describe('function fetchAnchor', function() { let fetchAnchor; let searchSourceStub; - beforeEach( - ngMock.inject(function createPrivateStubs() { - searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); - fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); - }) - ); + beforeEach(() => { + searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); + fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); + }); afterEach(() => { searchSourceStub._restore(); @@ -49,7 +40,7 @@ describe('context app', function() { { '@timestamp': 'desc' }, { _doc: 'desc' }, ]).then(() => { - expect(searchSourceStub.fetch.calledOnce).to.be(true); + expect(searchSourceStub.fetch.calledOnce).toBe(true); }); }); @@ -59,8 +50,8 @@ describe('context app', function() { { _doc: 'desc' }, ]).then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.calledOnce).to.be(true); - expect(setParentSpy.firstCall.args[0]).to.be(undefined); + expect(setParentSpy.calledOnce).toBe(true); + expect(setParentSpy.firstCall.args[0]).toBe(undefined); }); }); @@ -70,7 +61,7 @@ describe('context app', function() { { _doc: 'desc' }, ]).then(() => { const setFieldSpy = searchSourceStub.setField; - expect(setFieldSpy.firstCall.args[1].id).to.eql('INDEX_PATTERN_ID'); + expect(setFieldSpy.firstCall.args[1].id).toEqual('INDEX_PATTERN_ID'); }); }); @@ -80,8 +71,8 @@ describe('context app', function() { { _doc: 'desc' }, ]).then(() => { const setVersionSpy = searchSourceStub.setField.withArgs('version'); - expect(setVersionSpy.calledOnce).to.be(true); - expect(setVersionSpy.firstCall.args[1]).to.eql(true); + expect(setVersionSpy.calledOnce).toBe(true); + expect(setVersionSpy.firstCall.args[1]).toEqual(true); }); }); @@ -91,8 +82,8 @@ describe('context app', function() { { _doc: 'desc' }, ]).then(() => { const setSizeSpy = searchSourceStub.setField.withArgs('size'); - expect(setSizeSpy.calledOnce).to.be(true); - expect(setSizeSpy.firstCall.args[1]).to.eql(1); + expect(setSizeSpy.calledOnce).toBe(true); + expect(setSizeSpy.firstCall.args[1]).toEqual(1); }); }); @@ -102,8 +93,8 @@ describe('context app', function() { { _doc: 'desc' }, ]).then(() => { const setQuerySpy = searchSourceStub.setField.withArgs('query'); - expect(setQuerySpy.calledOnce).to.be(true); - expect(setQuerySpy.firstCall.args[1]).to.eql({ + expect(setQuerySpy.calledOnce).toBe(true); + expect(setQuerySpy.firstCall.args[1]).toEqual({ query: { constant_score: { filter: { @@ -124,8 +115,8 @@ describe('context app', function() { { _doc: 'desc' }, ]).then(() => { const setSortSpy = searchSourceStub.setField.withArgs('sort'); - expect(setSortSpy.calledOnce).to.be(true); - expect(setSortSpy.firstCall.args[1]).to.eql([{ '@timestamp': 'desc' }, { _doc: 'desc' }]); + expect(setSortSpy.calledOnce).toBe(true); + expect(setSortSpy.firstCall.args[1]).toEqual([{ '@timestamp': 'desc' }, { _doc: 'desc' }]); }); }); @@ -140,7 +131,7 @@ describe('context app', function() { expect().fail('expected the promise to be rejected'); }, error => { - expect(error).to.be.an(Error); + expect(error).toBeInstanceOf(Error); } ); }); @@ -152,8 +143,8 @@ describe('context app', function() { { '@timestamp': 'desc' }, { _doc: 'desc' }, ]).then(anchorDocument => { - expect(anchorDocument).to.have.property('property1', 'value1'); - expect(anchorDocument).to.have.property('$$_isAnchor', true); + expect(anchorDocument).toHaveProperty('property1', 'value1'); + expect(anchorDocument).toHaveProperty('$$_isAnchor', true); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js similarity index 74% rename from src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js index 02d998e8f4529..d6e91e57b22a8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js @@ -17,15 +17,10 @@ * under the License. */ -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; - import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; - -import { fetchContextProvider } from '../context'; +import { fetchContextProvider } from './context'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -34,46 +29,41 @@ const ANCHOR_TIMESTAMP_1000 = new Date(MS_PER_DAY * 1000).toJSON(); const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); describe('context app', function() { - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(ngMock.module('app/discover')); - describe('function fetchPredecessors', function() { let fetchPredecessors; let searchSourceStub; - beforeEach( - ngMock.inject(function createPrivateStubs() { - searchSourceStub = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); - fetchPredecessors = ( + beforeEach(() => { + searchSourceStub = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + fetchPredecessors = ( + indexPatternId, + timeField, + sortDir, + timeValIso, + timeValNr, + tieBreakerField, + tieBreakerValue, + size + ) => { + const anchor = { + _source: { + [timeField]: timeValIso, + }, + sort: [timeValNr, tieBreakerValue], + }; + + return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( + 'predecessors', indexPatternId, + anchor, timeField, - sortDir, - timeValIso, - timeValNr, tieBreakerField, - tieBreakerValue, - size - ) => { - const anchor = { - _source: { - [timeField]: timeValIso, - }, - sort: [timeValNr, tieBreakerValue], - }; - - return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( - 'predecessors', - indexPatternId, - anchor, - timeField, - tieBreakerField, - sortDir, - size, - [] - ); - }; - }) - ); + sortDir, + size, + [] + ); + }; + }); afterEach(() => { searchSourceStub._restore(); @@ -99,8 +89,8 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(searchSourceStub.fetch.calledOnce).to.be(true); - expect(hits).to.eql(searchSourceStub._stubHits.slice(0, 3)); + expect(searchSourceStub.fetch.calledOnce).toBe(true); + expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 3)); }); }); @@ -132,14 +122,14 @@ describe('context app', function() { expect( intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) - ).to.be(true); + ).toBe(true); // should have started at the given time - expect(intervals[0].gte).to.eql(moment(MS_PER_DAY * 3000).toISOString()); + expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have ended with a half-open interval - expect(_.last(intervals)).to.only.have.keys('gte', 'format'); - expect(intervals.length).to.be.greaterThan(1); + expect(Object.keys(_.last(intervals))).toEqual(['format', 'gte']); + expect(intervals.length).toBeGreaterThan(1); - expect(hits).to.eql(searchSourceStub._stubHits.slice(0, 3)); + expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 3)); }); }); @@ -169,11 +159,11 @@ describe('context app', function() { ); // should have started at the given time - expect(intervals[0].gte).to.eql(moment(MS_PER_DAY * 1000).toISOString()); + expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 1000).toISOString()); // should have stopped before reaching MS_PER_DAY * 1700 - expect(moment(_.last(intervals).lte).valueOf()).to.be.lessThan(MS_PER_DAY * 1700); - expect(intervals.length).to.be.greaterThan(1); - expect(hits).to.eql(searchSourceStub._stubHits.slice(-3)); + expect(moment(_.last(intervals).lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); + expect(intervals.length).toBeGreaterThan(1); + expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); }); }); @@ -189,7 +179,7 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(hits).to.eql([]); + expect(hits).toEqual([]); }); }); @@ -206,8 +196,8 @@ describe('context app', function() { [] ).then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); - expect(setParentSpy.called).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); + expect(setParentSpy.called).toBe(true); }); }); @@ -225,7 +215,7 @@ describe('context app', function() { ).then(() => { expect( searchSourceStub.setField.calledWith('sort', [{ '@timestamp': 'asc' }, { _doc: 'asc' }]) - ).to.be(true); + ).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js similarity index 74% rename from src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js index d4c00930c9383..cc2b6d31cb43b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js @@ -17,15 +17,12 @@ * under the License. */ -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; -import { fetchContextProvider } from '../context'; +import { fetchContextProvider } from './context'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -33,47 +30,42 @@ const ANCHOR_TIMESTAMP_3 = new Date(MS_PER_DAY * 3).toJSON(); const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); describe('context app', function() { - beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(ngMock.module('app/discover')); - describe('function fetchSuccessors', function() { let fetchSuccessors; let searchSourceStub; - beforeEach( - ngMock.inject(function createPrivateStubs() { - searchSourceStub = createContextSearchSourceStub([], '@timestamp'); + beforeEach(() => { + searchSourceStub = createContextSearchSourceStub([], '@timestamp'); + + fetchSuccessors = ( + indexPatternId, + timeField, + sortDir, + timeValIso, + timeValNr, + tieBreakerField, + tieBreakerValue, + size + ) => { + const anchor = { + _source: { + [timeField]: timeValIso, + }, + sort: [timeValNr, tieBreakerValue], + }; - fetchSuccessors = ( + return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( + 'successors', indexPatternId, + anchor, timeField, - sortDir, - timeValIso, - timeValNr, tieBreakerField, - tieBreakerValue, - size - ) => { - const anchor = { - _source: { - [timeField]: timeValIso, - }, - sort: [timeValNr, tieBreakerValue], - }; - - return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( - 'successors', - indexPatternId, - anchor, - timeField, - tieBreakerField, - sortDir, - size, - [] - ); - }; - }) - ); + sortDir, + size, + [] + ); + }; + }); afterEach(() => { searchSourceStub._restore(); @@ -99,8 +91,8 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(searchSourceStub.fetch.calledOnce).to.be(true); - expect(hits).to.eql(searchSourceStub._stubHits.slice(-3)); + expect(searchSourceStub.fetch.calledOnce).toBe(true); + expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); }); }); @@ -132,14 +124,14 @@ describe('context app', function() { expect( intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) - ).to.be(true); + ).toBe(true); // should have started at the given time - expect(intervals[0].lte).to.eql(moment(MS_PER_DAY * 3000).toISOString()); + expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have ended with a half-open interval - expect(_.last(intervals)).to.only.have.keys('lte', 'format'); - expect(intervals.length).to.be.greaterThan(1); + expect(Object.keys(_.last(intervals))).toEqual(['format', 'lte']); + expect(intervals.length).toBeGreaterThan(1); - expect(hits).to.eql(searchSourceStub._stubHits.slice(-3)); + expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); }); }); @@ -171,12 +163,12 @@ describe('context app', function() { ); // should have started at the given time - expect(intervals[0].lte).to.eql(moment(MS_PER_DAY * 3000).toISOString()); + expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have stopped before reaching MS_PER_DAY * 2200 - expect(moment(_.last(intervals).gte).valueOf()).to.be.greaterThan(MS_PER_DAY * 2200); - expect(intervals.length).to.be.greaterThan(1); + expect(moment(_.last(intervals).gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); + expect(intervals.length).toBeGreaterThan(1); - expect(hits).to.eql(searchSourceStub._stubHits.slice(0, 4)); + expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 4)); }); }); @@ -192,7 +184,7 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(hits).to.eql([]); + expect(hits).toEqual([]); }); }); @@ -209,8 +201,8 @@ describe('context app', function() { [] ).then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); - expect(setParentSpy.called).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); + expect(setParentSpy.called).toBe(true); }); }); @@ -228,7 +220,7 @@ describe('context app', function() { ).then(() => { expect( searchSourceStub.setField.calledWith('sort', [{ '@timestamp': 'desc' }, { _doc: 'desc' }]) - ).to.be(true); + ).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts index b91ef5a6b79fb..507f927c608e1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts @@ -17,14 +17,18 @@ * under the License. */ -import { IndexPattern, SearchSource } from '../../../../kibana_services'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { extractNanos, convertIsoToMillis } 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 { Filter, IndexPatternsContract } from '../../../../../../../../../plugins/data/public'; +import { + Filter, + IndexPatternsContract, + IndexPattern, + SearchSource, +} from '../../../../../../../../../plugins/data/public'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/fetch_hits_in_interval.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/fetch_hits_in_interval.ts index e7df44e6fe61c..8eed5d33ab004 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/fetch_hits_in_interval.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/fetch_hits_in_interval.ts @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { EsQuerySortValue, SortDirection, ISearchSource } from '../../../../../kibana_services'; +import { + ISearchSource, + EsQuerySortValue, + SortDirection, +} from '../../../../../../../../../../plugins/data/public'; import { convertTimeValueToIso } from './date_conversion'; import { EsHitRecordList } from '../context'; import { IntervalValue } from './generate_intervals'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/generate_intervals.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/generate_intervals.ts index 373dc37e56f6f..b14180d32b4f2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/generate_intervals.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/utils/generate_intervals.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from '../../../../../kibana_services'; +import { SortDirection } from '../../../../../../../../../../plugins/data/public'; export type IntervalValue = number | null; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js index 1cebb88cbda5a..674f40d0186e5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js @@ -29,9 +29,13 @@ import { FAILURE_REASONS, LOADING_STATUS } from './constants'; import { MarkdownSimple } from '../../../../../../../kibana_react/public'; export function QueryActionsProvider(Promise) { - const fetchAnchor = fetchAnchorProvider(getServices().indexPatterns, new SearchSource()); - const { fetchSurroundingDocs } = fetchContextProvider(getServices().indexPatterns); - const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions(); + const { filterManager, indexPatterns } = getServices(); + const fetchAnchor = fetchAnchorProvider(indexPatterns, new SearchSource()); + const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns); + const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions( + filterManager, + indexPatterns + ); const setFailedStatus = state => (subject, details = {}) => (state.loadingStatus[subject] = { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js index 5be1179a9ae09..5c1700e776361 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js @@ -18,14 +18,11 @@ */ import _ from 'lodash'; -import { getServices } from '../../../../kibana_services'; import { esFilters } from '../../../../../../../../../plugins/data/public'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants'; -export function getQueryParameterActions() { - const filterManager = getServices().filterManager; - +export function getQueryParameterActions(filterManager, indexPatterns) { const setPredecessorCount = state => predecessorCount => (state.queryParameters.predecessorCount = clamp( MIN_CONTEXT_SIZE, @@ -57,8 +54,10 @@ export function getQueryParameterActions() { indexPatternId ); filterManager.addFilters(newFilters); - const indexPattern = await getServices().indexPatterns.get(indexPatternId); - indexPattern.popularizeField(field.name, 1); + if (indexPatterns) { + const indexPattern = await indexPatterns.get(indexPatternId); + indexPattern.popularizeField(field.name, 1); + } }; return { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.test.ts new file mode 100644 index 0000000000000..35fbd33fb4bc9 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.test.ts @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { getQueryParameterActions } from './actions'; +import { FilterManager } from '../../../../../../../../../plugins/data/public'; +import { coreMock } from '../../../../../../../../../core/public/mocks'; +const setupMock = coreMock.createSetup(); + +let state: { + queryParameters: { + defaultStepSize: number; + indexPatternId: string; + predecessorCount: number; + successorCount: number; + }; +}; +let filterManager: FilterManager; +let filterManagerSpy: jest.SpyInstance; + +beforeEach(() => { + filterManager = new FilterManager(setupMock.uiSettings); + filterManagerSpy = jest.spyOn(filterManager, 'addFilters'); + + state = { + queryParameters: { + defaultStepSize: 3, + indexPatternId: 'INDEX_PATTERN_ID', + predecessorCount: 10, + successorCount: 10, + }, + }; +}); + +describe('context query_parameter actions', function() { + describe('action addFilter', () => { + it('should pass the given arguments to the filterManager', () => { + const { addFilter } = getQueryParameterActions(filterManager); + + addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); + + // get the generated filter + const generatedFilter = filterManagerSpy.mock.calls[0][0][0]; + const queryKeys = Object.keys(generatedFilter.query.match_phrase); + expect(filterManagerSpy.mock.calls.length).toBe(1); + expect(queryKeys[0]).toBe('FIELD_NAME'); + expect(generatedFilter.query.match_phrase[queryKeys[0]]).toBe('FIELD_VALUE'); + }); + + it('should pass the index pattern id to the filterManager', () => { + const { addFilter } = getQueryParameterActions(filterManager); + addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); + const generatedFilter = filterManagerSpy.mock.calls[0][0][0]; + expect(generatedFilter.meta.index).toBe('INDEX_PATTERN_ID'); + }); + }); + describe('action setPredecessorCount', () => { + it('should set the predecessorCount to the given value', () => { + const { setPredecessorCount } = getQueryParameterActions(filterManager); + setPredecessorCount(state)(20); + expect(state.queryParameters.predecessorCount).toBe(20); + }); + + it('should limit the predecessorCount to 0 as a lower bound', () => { + const { setPredecessorCount } = getQueryParameterActions(filterManager); + setPredecessorCount(state)(-1); + expect(state.queryParameters.predecessorCount).toBe(0); + }); + + it('should limit the predecessorCount to 10000 as an upper bound', () => { + const { setPredecessorCount } = getQueryParameterActions(filterManager); + setPredecessorCount(state)(20000); + expect(state.queryParameters.predecessorCount).toBe(10000); + }); + }); + describe('action setSuccessorCount', () => { + it('should set the successorCount to the given value', function() { + const { setSuccessorCount } = getQueryParameterActions(filterManager); + setSuccessorCount(state)(20); + + expect(state.queryParameters.successorCount).toBe(20); + }); + + it('should limit the successorCount to 0 as a lower bound', () => { + const { setSuccessorCount } = getQueryParameterActions(filterManager); + setSuccessorCount(state)(-1); + expect(state.queryParameters.successorCount).toBe(0); + }); + + it('should limit the successorCount to 10000 as an upper bound', () => { + const { setSuccessorCount } = getQueryParameterActions(filterManager); + setSuccessorCount(state)(20000); + expect(state.queryParameters.successorCount).toBe(10000); + }); + }); + describe('action setQueryParameters', function() { + const { setQueryParameters } = getQueryParameterActions(filterManager); + + it('should update the queryParameters with valid properties from the given object', function() { + const newState = { + ...state, + queryParameters: { + additionalParameter: 'ADDITIONAL_PARAMETER', + }, + }; + + const actualState = setQueryParameters(newState)({ + anchorId: 'ANCHOR_ID', + columns: ['column'], + defaultStepSize: 3, + filters: ['filter'], + indexPatternId: 'INDEX_PATTERN', + predecessorCount: 100, + successorCount: 100, + sort: ['field'], + }); + + expect(actualState).toEqual({ + additionalParameter: 'ADDITIONAL_PARAMETER', + anchorId: 'ANCHOR_ID', + columns: ['column'], + defaultStepSize: 3, + filters: ['filter'], + indexPatternId: 'INDEX_PATTERN', + predecessorCount: 100, + successorCount: 100, + sort: ['field'], + }); + }); + + it('should ignore invalid properties', function() { + const newState = { ...state }; + + setQueryParameters(newState)({ + additionalParameter: 'ADDITIONAL_PARAMETER', + }); + + expect(state.queryParameters).toEqual(newState.queryParameters); + }); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js index b5ba2844e8b06..345717cafee9a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { getAngularModule } from '../../kibana_services'; +import { getAngularModule, getServices } from '../../kibana_services'; import contextAppTemplate from './context_app.html'; import './context/components/action_bar'; import { getFirstSortableField } from './context/api/utils/sorting'; @@ -58,7 +58,8 @@ module.directive('contextApp', function ContextApp() { }); function ContextAppController($scope, config, Private) { - const queryParameterActions = getQueryParameterActions(); + const { filterManager, indexpatterns } = getServices(); + const queryParameterActions = getQueryParameterActions(filterManager, indexpatterns); const queryActions = Private(QueryActionsProvider); this.state = createInitialState( parseInt(config.get('context:step'), 10), diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js index 1a3922dfc2008..5482258e06564 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js @@ -19,7 +19,6 @@ import { DiscoverNoResults } from './no_results'; import { DiscoverUninitialized } from './uninitialized'; -import { DiscoverUnsupportedIndexPattern } from './unsupported_index_pattern'; import { DiscoverHistogram } from './histogram'; import { getAngularModule, wrapInI18nContext } from '../../../kibana_services'; @@ -33,8 +32,4 @@ app.directive('discoverUninitialized', reactDirective => reactDirective(wrapInI18nContext(DiscoverUninitialized)) ); -app.directive('discoverUnsupportedIndexPattern', reactDirective => - reactDirective(wrapInI18nContext(DiscoverUnsupportedIndexPattern), ['unsupportedType']) -); - app.directive('discoverHistogram', reactDirective => reactDirective(DiscoverHistogram)); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/unsupported_index_pattern.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/unsupported_index_pattern.js deleted file mode 100644 index 7cf4fd1d14181..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/unsupported_index_pattern.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Fragment } from 'react'; -import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const DiscoverUnsupportedIndexPattern = ({ unsupportedType }) => { - // This message makes the assumption that X-Pack will support this type, as is the case with - // rollup index patterns. - const message = ( - - ); - - return ( - - - - - - - - - - ); -}; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index 2d44b12989228..2334e33deadba 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -38,11 +38,6 @@

{{screenTitle}}

- - !_.isEqual(newStatePartial[prop], oldStatePartial[prop]) ); - if (changes.indexOf('index') !== -1) { - try { - $scope.indexPattern = await indexPatterns.get(newStatePartial.index); - $scope.opts.timefield = getTimeField(); - $scope.enableTimeRangeSelector = !!$scope.opts.timefield; - // is needed to rerender the histogram - $scope.vis = undefined; - - // Taking care of sort when switching index pattern: - // Old indexPattern: sort by A - // If A is not available in the new index pattern, sort has to be adapted and propagated to URL - const sort = getSortArray(newStatePartial.sort, $scope.indexPattern); - if (newStatePartial.sort && !_.isEqual(sort, newStatePartial.sort)) { - return await replaceUrlAppState({ sort }); - } - } catch (e) { - toastNotifications.addWarning({ text: getIndexPatternWarning(newStatePartial.index) }); - } - } if (changes.length) { $fetchObservable.next(); @@ -268,8 +248,9 @@ function discoverController( } }); - $scope.setIndexPattern = id => { - setAppState({ index: id }); + $scope.setIndexPattern = async id => { + await replaceUrlAppState({ index: id }); + $route.reload(); }; // update data source when filters update @@ -805,7 +786,7 @@ function discoverController( title: i18n.translate('kbn.discover.errorLoadingData', { defaultMessage: 'Error loading data', }), - toastMessage: error.shortMessage, + toastMessage: error.shortMessage || error.body?.message, }); } }); @@ -1107,17 +1088,6 @@ function discoverController( return loadedIndexPattern; } - // Block the UI from loading if the user has loaded a rollup index pattern but it isn't - // supported. - $scope.isUnsupportedIndexPattern = - !isDefaultType($route.current.locals.savedObjects.ip.loaded) && - !hasSearchStategyForIndexPattern($route.current.locals.savedObjects.ip.loaded); - - if ($scope.isUnsupportedIndexPattern) { - $scope.unsupportedIndexPatternType = $route.current.locals.savedObjects.ip.loaded.type; - return; - } - addHelpMenuToAppChrome(chrome); init(); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index e1a20e3381331..3fab650002c17 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -115,7 +115,7 @@ function VisualizeAppController( savedVis.vis.on('apply', _applyVis); // vis is instance of src/legacy/ui/public/vis/vis.js. // SearchSource is a promise-based stream of search results that can inherit from other search sources. - const { vis, searchSource } = savedVis; + const { vis, searchSource, savedSearch } = savedVis; $scope.vis = vis; @@ -379,6 +379,17 @@ function VisualizeAppController( }, }; + const handleLinkedSearch = linked => { + if (linked && !savedVis.savedSearchId && savedSearch) { + savedVis.savedSearchId = savedSearch.id; + vis.savedSearchId = savedSearch.id; + searchSource.setParent(savedSearch.searchSource); + } else if (!linked && savedVis.savedSearchId) { + delete savedVis.savedSearchId; + delete vis.savedSearchId; + } + }; + // Create a PersistedState instance for uiState. const { persistedState, unsubscribePersisted, persistOnChange } = makeStateful( 'uiState', @@ -387,9 +398,9 @@ function VisualizeAppController( $scope.uiState = persistedState; $scope.savedVis = savedVis; $scope.query = initialState.query; - $scope.linked = initialState.linked; $scope.searchSource = searchSource; $scope.refreshInterval = timefilter.getRefreshInterval(); + handleLinkedSearch(initialState.linked); const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; @@ -468,7 +479,7 @@ function VisualizeAppController( $scope.fetch = function() { const { query, linked, filters } = stateContainer.getState(); $scope.query = query; - $scope.linked = linked; + handleLinkedSearch(linked); savedVis.searchSource.setField('query', query); savedVis.searchSource.setField('filter', filters); $scope.$broadcast('render'); @@ -558,20 +569,6 @@ function VisualizeAppController( updateStateFromSavedQuery(savedQuery); }; - $scope.$watch('linked', linked => { - if (linked && !savedVis.savedSearchId) { - savedVis.savedSearchId = savedVis.searchSource.id; - vis.savedSearchId = savedVis.searchSource.id; - - $scope.$broadcast('render'); - } else if (!linked && savedVis.savedSearchId) { - delete savedVis.savedSearchId; - delete vis.savedSearchId; - - $scope.$broadcast('render'); - } - }); - /** * Called when the user clicks "Save" button. */ @@ -663,33 +660,26 @@ function VisualizeAppController( } const unlinkFromSavedSearch = () => { - const searchSourceParent = searchSource.getParent(); + const searchSourceParent = savedSearch.searchSource; const searchSourceGrandparent = searchSourceParent.getParent(); + const currentIndex = searchSourceParent.getField('index'); - delete savedVis.savedSearchId; - delete vis.savedSearchId; - searchSourceParent.setField( - 'filter', - _.union(searchSource.getOwnField('filter'), searchSourceParent.getOwnField('filter')) - ); - - stateContainer.transitions.unlinkSavedSearch( - searchSourceParent.getField('query'), - searchSourceParent.getField('filter') - ); - searchSource.setField('index', searchSourceParent.getField('index')); + searchSource.setField('index', currentIndex); searchSource.setParent(searchSourceGrandparent); + stateContainer.transitions.unlinkSavedSearch({ + query: searchSourceParent.getField('query'), + parentFilters: searchSourceParent.getOwnField('filter'), + }); + toastNotifications.addSuccess( i18n.translate('kbn.visualize.linkedToSearch.unlinkSuccessNotificationText', { defaultMessage: `Unlinked from saved search '{searchTitle}'`, values: { - searchTitle: savedVis.savedSearch.title, + searchTitle: savedSearch.title, }, }) ); - - $scope.fetch(); }; $scope.getAdditionalMessage = () => { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts index d3fae3d457b63..86f39ea76dd3a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isFunction, omit } from 'lodash'; +import { isFunction, omit, union } from 'lodash'; import { migrateAppState } from './migrate_app_state'; import { @@ -75,10 +75,10 @@ export function useVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Argu query: defaultQuery, }; }, - unlinkSavedSearch: state => (query, filters) => ({ + unlinkSavedSearch: state => ({ query, parentFilters = [] }) => ({ ...state, - query, - filters, + query: query || state.query, + filters: union(state.filters, parentFilters), linked: false, }), updateVisState: state => newVisState => ({ ...state, vis: toObject(newVisState) }), diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 55fccd75361a0..ccb3b3ddbb1da 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -52,7 +52,7 @@ export interface VisualizeAppStateTransitions { removeSavedQuery: (state: VisualizeAppState) => (defaultQuery: Query) => VisualizeAppState; unlinkSavedSearch: ( state: VisualizeAppState - ) => (query: Query, filters: Filter[]) => VisualizeAppState; + ) => ({ query, parentFilters }: { query?: Query; parentFilters?: Filter[] }) => VisualizeAppState; updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; updateFromSavedQuery: (state: VisualizeAppState) => (savedQuery: SavedQuery) => VisualizeAppState; } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/application_usage/index.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/application_usage/index.test.ts index cdfead2dff3c6..1a64100bda692 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/application_usage/index.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/application_usage/index.test.ts @@ -114,9 +114,11 @@ describe('telemetry_application_usage', () => { expect(await collector.fetch(callCluster)).toStrictEqual({ appId: { clicks_total: total - 1 + 10, + clicks_7_days: total - 1, clicks_30_days: total - 1, clicks_90_days: total - 1, minutes_on_screen_total: total - 1 + 10, + minutes_on_screen_7_days: total - 1, minutes_on_screen_30_days: total - 1, minutes_on_screen_90_days: total - 1, }, diff --git a/src/legacy/core_plugins/telemetry/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/application_usage/telemetry_application_usage_collector.ts index 5047ebc4b0454..5c862686a37d9 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -20,12 +20,8 @@ import moment from 'moment'; import { APPLICATION_USAGE_TYPE } from '../../../common/constants'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; -import { - ISavedObjectsRepository, - SavedObjectAttributes, - SavedObjectsFindOptions, - SavedObject, -} from '../../../../../../core/server'; +import { ISavedObjectsRepository, SavedObjectAttributes } from '../../../../../../core/server'; +import { findAll } from '../find_all'; /** * Roll indices every 24h @@ -53,30 +49,16 @@ interface ApplicationUsageTransactional extends ApplicationUsageTotal { interface ApplicationUsageTelemetryReport { [appId: string]: { clicks_total: number; + clicks_7_days: number; clicks_30_days: number; clicks_90_days: number; minutes_on_screen_total: number; + minutes_on_screen_7_days: number; minutes_on_screen_30_days: number; minutes_on_screen_90_days: number; }; } -async function findAll( - savedObjectsClient: ISavedObjectsRepository, - opts: SavedObjectsFindOptions -): Promise>> { - const { page = 1, perPage = 100, ...options } = opts; - const { saved_objects: savedObjects, total } = await savedObjectsClient.find({ - ...options, - page, - perPage, - }); - if (page * perPage >= total) { - return savedObjects; - } - return [...savedObjects, ...(await findAll(savedObjectsClient, { ...opts, page: page + 1 }))]; -} - export function registerApplicationUsageCollector( usageCollection: UsageCollectionSetup, getSavedObjectsClient: () => ISavedObjectsRepository | undefined @@ -103,9 +85,11 @@ export function registerApplicationUsageCollector( ...acc, [appId]: { clicks_total: numberOfClicks + existing.clicks_total, + clicks_7_days: 0, clicks_30_days: 0, clicks_90_days: 0, minutes_on_screen_total: minutesOnScreen + existing.minutes_on_screen_total, + minutes_on_screen_7_days: 0, minutes_on_screen_30_days: 0, minutes_on_screen_90_days: 0, }, @@ -113,7 +97,7 @@ export function registerApplicationUsageCollector( }, {} as ApplicationUsageTelemetryReport ); - + const nowMinus7 = moment().subtract(7, 'days'); const nowMinus30 = moment().subtract(30, 'days'); const nowMinus90 = moment().subtract(90, 'days'); @@ -121,17 +105,24 @@ export function registerApplicationUsageCollector( (acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => { const existing = acc[appId] || { clicks_total: 0, + clicks_7_days: 0, clicks_30_days: 0, clicks_90_days: 0, minutes_on_screen_total: 0, + minutes_on_screen_7_days: 0, minutes_on_screen_30_days: 0, minutes_on_screen_90_days: 0, }; const timeOfEntry = moment(timestamp as string); + const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7); const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30); const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90); + const last7Days = { + clicks_7_days: existing.clicks_7_days + numberOfClicks, + minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen, + }; const last30Days = { clicks_30_days: existing.clicks_30_days + numberOfClicks, minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen, @@ -147,6 +138,7 @@ export function registerApplicationUsageCollector( ...existing, clicks_total: existing.clicks_total + numberOfClicks, minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen, + ...(isInLast7Days ? last7Days : {}), ...(isInLast30Days ? last30Days : {}), ...(isInLast90Days ? last90Days : {}), }, diff --git a/src/legacy/core_plugins/telemetry/server/collectors/find_all.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/find_all.test.ts new file mode 100644 index 0000000000000..012cda395bc6c --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/find_all.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; + +import { findAll } from './find_all'; + +describe('telemetry_application_usage', () => { + test('when savedObjectClient is initialised, return something', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation( + async () => + ({ + saved_objects: [], + total: 0, + } as any) + ); + + expect(await findAll(savedObjectClient, { type: 'test-type' })).toStrictEqual([]); + }); + + test('paging in findAll works', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + let total = 201; + const doc = { id: 'test-id', attributes: { test: 1 } }; + savedObjectClient.find.mockImplementation(async opts => { + if ((opts.page || 1) > 2) { + return { saved_objects: [], total } as any; + } + const savedObjects = new Array(opts.perPage).fill(doc); + total = savedObjects.length * 2 + 1; + return { saved_objects: savedObjects, total }; + }); + + expect(await findAll(savedObjectClient, { type: 'test-type' })).toStrictEqual( + new Array(total - 1).fill(doc) + ); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/_utils.js b/src/legacy/core_plugins/telemetry/server/collectors/find_all.ts similarity index 55% rename from src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/_utils.js rename to src/legacy/core_plugins/telemetry/server/collectors/find_all.ts index 63f8ced97e9dc..e6363551eba9c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/_utils.js +++ b/src/legacy/core_plugins/telemetry/server/collectors/find_all.ts @@ -17,18 +17,25 @@ * under the License. */ -import _ from 'lodash'; +import { + SavedObjectAttributes, + ISavedObjectsRepository, + SavedObjectsFindOptions, + SavedObject, +} from 'kibana/server'; -export function createStateStub(overrides) { - return _.merge( - { - queryParameters: { - defaultStepSize: 3, - indexPatternId: 'INDEX_PATTERN_ID', - predecessorCount: 10, - successorCount: 10, - }, - }, - overrides - ); +export async function findAll( + savedObjectsClient: ISavedObjectsRepository, + opts: SavedObjectsFindOptions +): Promise>> { + const { page = 1, perPage = 100, ...options } = opts; + const { saved_objects: savedObjects, total } = await savedObjectsClient.find({ + ...options, + page, + perPage, + }); + if (page * perPage >= total) { + return savedObjects; + } + return [...savedObjects, ...(await findAll(savedObjectsClient, { ...opts, page: page + 1 }))]; } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.test.ts new file mode 100644 index 0000000000000..ddb58a7d09bbd --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; +import { savedObjectsRepositoryMock } from '../../../../../../core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CollectorOptions } from '../../../../../../plugins/usage_collection/server/collector/collector'; + +import { registerUiMetricUsageCollector } from './'; + +describe('telemetry_ui_metric', () => { + let collector: CollectorOptions; + + const usageCollectionMock: jest.Mocked = { + makeUsageCollector: jest.fn().mockImplementation(config => (collector = config)), + registerCollector: jest.fn(), + } as any; + + const getUsageCollector = jest.fn(); + const callCluster = jest.fn(); + + beforeAll(() => registerUiMetricUsageCollector(usageCollectionMock, getUsageCollector)); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + }); + + test('if no savedObjectClient initialised, return undefined', async () => { + expect(await collector.fetch(callCluster)).toBeUndefined(); + }); + + test('when savedObjectClient is initialised, return something', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation( + async () => + ({ + saved_objects: [], + total: 0, + } as any) + ); + getUsageCollector.mockImplementation(() => savedObjectClient); + + expect(await collector.fetch(callCluster)).toStrictEqual({}); + expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); + }); + + test('results grouped by appName', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async () => { + return { + saved_objects: [ + { id: 'testAppName:testKeyName1', attributes: { count: 3 } }, + { id: 'testAppName:testKeyName2', attributes: { count: 5 } }, + { id: 'testAppName2:testKeyName3', attributes: { count: 1 } }, + ], + total: 3, + } as any; + }); + + getUsageCollector.mockImplementation(() => savedObjectClient); + + expect(await collector.fetch(callCluster)).toStrictEqual({ + testAppName: [ + { key: 'testKeyName1', value: 3 }, + { key: 'testKeyName2', value: 5 }, + ], + testAppName2: [{ key: 'testKeyName3', value: 1 }], + }); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index 73157abce8629..a7b6850b0b20a 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -17,24 +17,33 @@ * under the License. */ +import { ISavedObjectsRepository, SavedObjectAttributes } from 'kibana/server'; import { UI_METRIC_USAGE_TYPE } from '../../../common/constants'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; +import { findAll } from '../find_all'; -export function registerUiMetricUsageCollector(usageCollection: UsageCollectionSetup, server: any) { +interface UIMetricsSavedObjects extends SavedObjectAttributes { + count: number; +} + +export function registerUiMetricUsageCollector( + usageCollection: UsageCollectionSetup, + getSavedObjectsClient: () => ISavedObjectsRepository | undefined +) { const collector = usageCollection.makeUsageCollector({ type: UI_METRIC_USAGE_TYPE, fetch: async () => { - const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - const savedObjectsClient = new SavedObjectsClient(internalRepository); + const savedObjectsClient = getSavedObjectsClient(); + if (typeof savedObjectsClient === 'undefined') { + return; + } - const { saved_objects: rawUiMetrics } = await savedObjectsClient.find({ + const rawUiMetrics = await findAll(savedObjectsClient, { type: 'ui-metric', fields: ['count'], }); - const uiMetricsByAppName = rawUiMetrics.reduce((accum: any, rawUiMetric: any) => { + const uiMetricsByAppName = rawUiMetrics.reduce((accum, rawUiMetric) => { const { id, attributes: { count }, @@ -42,18 +51,16 @@ export function registerUiMetricUsageCollector(usageCollection: UsageCollectionS const [appName, metricType] = id.split(':'); - if (!accum[appName]) { - accum[appName] = []; - } - const pair = { key: metricType, value: count }; - accum[appName].push(pair); - return accum; - }, {}); + return { + ...accum, + [appName]: [...(accum[appName] || []), pair], + }; + }, {} as Record>); return uiMetricsByAppName; }, - isReady: () => true, + isReady: () => typeof getSavedObjectsClient() !== 'undefined', }); usageCollection.registerCollector(collector); diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts index d859c0cfd4678..0b9f0526988c8 100644 --- a/src/legacy/core_plugins/telemetry/server/plugin.ts +++ b/src/legacy/core_plugins/telemetry/server/plugin.ts @@ -59,7 +59,7 @@ export class TelemetryPlugin { registerTelemetryPluginUsageCollector(usageCollection, server); registerLocalizationUsageCollector(usageCollection, server); registerTelemetryUsageCollector(usageCollection, server); - registerUiMetricUsageCollector(usageCollection, server); + registerUiMetricUsageCollector(usageCollection, getSavedObjectsClient); registerManagementUsageCollector(usageCollection, server); registerApplicationUsageCollector(usageCollection, getSavedObjectsClient); } diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts index ccef24f8f9746..ac7e407ca9e4d 100644 --- a/src/legacy/core_plugins/vis_type_vega/index.ts +++ b/src/legacy/core_plugins/vis_type_vega/index.ts @@ -24,10 +24,16 @@ import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy const vegaPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => new Plugin({ - // TODO: ID property should be changed from 'vega' to 'vis_type_vega' - // It is required to change the configuration property - // vega.enableExternalUrls -> vis_type_vega.enableExternalUrls - id: 'vega', + id: 'vis_type_vega', + deprecations: ({ rename }: { rename: any }) => [ + rename('vega.enabled', 'vis_type_vega.enabled'), + ], + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + enableExternalUrls: Joi.boolean().default(false), + }).default(); + }, require: ['kibana', 'elasticsearch'], publicDir: resolve(__dirname, 'public'), uiExports: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts index f658f6ef52df8..ba86125f2e246 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts @@ -32,6 +32,12 @@ export interface Vis { aggs: Array<{ [key: string]: any }>; }; + /** + * If a visualization based on the saved search, + * the id is necessary for building an expression function in src/plugins/expressions/common/expression_functions/specs/kibana_context.ts + */ + savedSearchId?: string; + // Since we haven't typed everything here yet, we basically "any" the rest // of that interface. This should be removed as soon as this type definition // has been completed. But that way we at least have typing for a couple of diff --git a/src/legacy/ui/public/filter_manager/index.js b/src/legacy/ui/public/filter_manager/index.js deleted file mode 100644 index 9880b336e76e5..0000000000000 --- a/src/legacy/ui/public/filter_manager/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ diff --git a/src/legacy/ui/public/filter_manager/query_filter.d.ts b/src/legacy/ui/public/filter_manager/query_filter.d.ts deleted file mode 100644 index b5d7742f51d46..0000000000000 --- a/src/legacy/ui/public/filter_manager/query_filter.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type QueryFilter = any; - -export const FilterBarQueryFilterProvider: () => QueryFilter; diff --git a/src/legacy/ui/public/filter_manager/query_filter.js b/src/legacy/ui/public/filter_manager/query_filter.js deleted file mode 100644 index 97b3810b7f1c7..0000000000000 --- a/src/legacy/ui/public/filter_manager/query_filter.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { FilterStateManager } from 'plugins/data'; -import { npStart } from 'ui/new_platform'; - -export function FilterBarQueryFilterProvider(getAppState, globalState) { - const { filterManager } = npStart.plugins.data.query; - const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); - - const queryFilter = {}; - queryFilter.getUpdates$ = filterManager.getUpdates$.bind(filterManager); - queryFilter.getFetches$ = filterManager.getFetches$.bind(filterManager); - queryFilter.getFilters = filterManager.getFilters.bind(filterManager); - queryFilter.getAppFilters = filterManager.getAppFilters.bind(filterManager); - queryFilter.getGlobalFilters = filterManager.getGlobalFilters.bind(filterManager); - queryFilter.removeFilter = filterManager.removeFilter.bind(filterManager); - queryFilter.addFilters = filterManager.addFilters.bind(filterManager); - queryFilter.setFilters = filterManager.setFilters.bind(filterManager); - queryFilter.removeAll = filterManager.removeAll.bind(filterManager); - - queryFilter.destroy = () => { - filterManager.destroy(); - filterStateManager.destroy(); - }; - - return queryFilter; -} diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index 1576a6e38e36d..550aac6819d0b 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -23,6 +23,7 @@ export const ES_SEARCH_STRATEGY = 'es'; export interface IEsSearchRequest extends IKibanaSearchRequest { params: SearchParams; + indexType?: string; } export interface IEsSearchResponse extends IKibanaSearchResponse { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 7487f048525bd..7d739103eb2bb 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -286,11 +286,7 @@ export { export { ES_SEARCH_STRATEGY, SYNC_SEARCH_STRATEGY, - defaultSearchStrategy, - esSearchStrategyProvider, getEsPreference, - addSearchStrategy, - hasSearchStategyForIndexPattern, getSearchErrorType, ISearchContext, TSearchStrategyProvider, @@ -348,9 +344,7 @@ export { SavedQuery, SavedQueryService, SavedQueryTimeFilter, - SavedQueryAttributes, InputTimeRange, - TimefilterSetup, TimeHistory, TimefilterContract, TimeHistoryContract, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index c41a4ef531443..48125254ff749 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -49,11 +49,6 @@ import { UiSettingsParams } from 'src/core/server/types'; import { UnregisterCallback } from 'history'; import { UserProvidedValues } from 'src/core/server/types'; -// Warning: (ae-missing-release-tag) "addSearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const addSearchStrategy: (searchStrategy: SearchStrategyProvider) => void; - // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -153,11 +148,6 @@ export interface DataPublicPluginStart { }; } -// Warning: (ae-missing-release-tag) "defaultSearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const defaultSearchStrategy: SearchStrategyProvider; - // @public (undocumented) export enum ES_FIELD_TYPES { // (undocumented) @@ -313,11 +303,6 @@ export interface EsQueryConfig { // @public (undocumented) export type EsQuerySortValue = Record; -// Warning: (ae-missing-release-tag) "esSearchStrategyProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const esSearchStrategyProvider: TSearchStrategyProvider; - // Warning: (ae-missing-release-tag) "ExistsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -488,11 +473,6 @@ export function getSearchErrorType({ message }: Pick): " // @public (undocumented) export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; -// Warning: (ae-missing-release-tag) "hasSearchStategyForIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const hasSearchStategyForIndexPattern: (indexPattern: IndexPattern) => boolean; - // Warning: (ae-missing-release-tag) "IDataPluginServices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -517,6 +497,8 @@ export interface IDataPluginServices extends Partial { // // @public (undocumented) export interface IEsSearchRequest extends IKibanaSearchRequest { + // (undocumented) + indexType?: string; // (undocumented) params: SearchParams; } @@ -1227,28 +1209,14 @@ export interface RefreshInterval { // // @public (undocumented) export interface SavedQuery { + // Warning: (ae-forgotten-export) The symbol "SavedQueryAttributes" needs to be exported by the entry point index.d.ts + // // (undocumented) attributes: SavedQueryAttributes; // (undocumented) id: string; } -// Warning: (ae-missing-release-tag) "SavedQueryAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface SavedQueryAttributes { - // (undocumented) - description: string; - // (undocumented) - filters?: Filter[]; - // (undocumented) - query: Query; - // (undocumented) - timefilter?: SavedQueryTimeFilter; - // (undocumented) - title: string; -} - // Warning: (ae-missing-release-tag) "SavedQueryService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1484,14 +1452,6 @@ export const syncQueryStateWithUrl: (query: Pick<{ // @public (undocumented) export type TimefilterContract = PublicMethodsOf; -// @public (undocumented) -export interface TimefilterSetup { - // (undocumented) - history: TimeHistoryContract; - // (undocumented) - timefilter: TimefilterContract; -} - // Warning: (ae-missing-release-tag) "TimeHistory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts index c983cc4ea8fc5..a86a5b4ed401e 100644 --- a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts @@ -18,8 +18,8 @@ */ import { createSavedQueryService } from './saved_query_service'; -import { SavedQueryAttributes } from '../..'; import { FilterStateStore } from '../../../common'; +import { SavedQueryAttributes } from './types'; const savedQueryAttributes: SavedQueryAttributes = { title: 'foo', diff --git a/src/plugins/data/public/search/fetch/call_client.test.ts b/src/plugins/data/public/search/fetch/call_client.test.ts index 6b43157aab83b..7a99b7c064515 100644 --- a/src/plugins/data/public/search/fetch/call_client.test.ts +++ b/src/plugins/data/public/search/fetch/call_client.test.ts @@ -20,60 +20,35 @@ import { callClient } from './call_client'; import { handleResponse } from './handle_response'; import { FetchHandlers } from './types'; -import { SearchRequest } from '../..'; -import { SearchStrategySearchParams } from '../search_strategy'; - -const mockResponses = [{}, {}]; -const mockAbortFns = [jest.fn(), jest.fn()]; -const mockSearchFns = [ - jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ - searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[0])), - abort: mockAbortFns[0], - })), - jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ - searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[1])), - abort: mockAbortFns[1], - })), -]; -const mockSearchStrategies = mockSearchFns.map((search, i) => ({ search, id: i })); +import { SearchStrategySearchParams, defaultSearchStrategy } from '../search_strategy'; +const mockAbortFn = jest.fn(); jest.mock('./handle_response', () => ({ handleResponse: jest.fn((request, response) => response), })); -jest.mock('../search_strategy', () => ({ - getSearchStrategyForSearchRequest: (request: SearchRequest) => - mockSearchStrategies[request._searchStrategyId], - getSearchStrategyById: (id: number) => mockSearchStrategies[id], -})); +jest.mock('../search_strategy', () => { + return { + defaultSearchStrategy: { + search: jest.fn(({ searchRequests }: SearchStrategySearchParams) => { + return { + searching: Promise.resolve( + searchRequests.map(req => { + return { + id: req._searchStrategyId, + }; + }) + ), + abort: mockAbortFn, + }; + }), + }, + }; +}); describe('callClient', () => { beforeEach(() => { (handleResponse as jest.Mock).mockClear(); - mockAbortFns.forEach(fn => fn.mockClear()); - mockSearchFns.forEach(fn => fn.mockClear()); - }); - - test('Executes each search strategy with its group of matching requests', () => { - const searchRequests = [ - { _searchStrategyId: 0 }, - { _searchStrategyId: 1 }, - { _searchStrategyId: 0 }, - { _searchStrategyId: 1 }, - ]; - - callClient(searchRequests, [], {} as FetchHandlers); - - expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([ - searchRequests[0], - searchRequests[2], - ]); - expect(mockSearchFns[1]).toBeCalled(); - expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([ - searchRequests[1], - searchRequests[3], - ]); }); test('Passes the additional arguments it is given to the search strategy', () => { @@ -82,8 +57,11 @@ describe('callClient', () => { callClient(searchRequests, [], args); - expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0]).toEqual({ searchRequests, ...args }); + expect(defaultSearchStrategy.search).toBeCalled(); + expect((defaultSearchStrategy.search as any).mock.calls[0][0]).toEqual({ + searchRequests, + ...args, + }); }); test('Returns the responses in the original order', async () => { @@ -91,7 +69,8 @@ describe('callClient', () => { const responses = await Promise.all(callClient(searchRequests, [], {} as FetchHandlers)); - expect(responses).toEqual([mockResponses[1], mockResponses[0]]); + expect(responses[0]).toEqual({ id: searchRequests[0]._searchStrategyId }); + expect(responses[1]).toEqual({ id: searchRequests[1]._searchStrategyId }); }); test('Calls handleResponse with each request and response', async () => { @@ -101,8 +80,12 @@ describe('callClient', () => { await Promise.all(responses); expect(handleResponse).toBeCalledTimes(2); - expect(handleResponse).toBeCalledWith(searchRequests[0], mockResponses[0]); - expect(handleResponse).toBeCalledWith(searchRequests[1], mockResponses[1]); + expect(handleResponse).toBeCalledWith(searchRequests[0], { + id: searchRequests[0]._searchStrategyId, + }); + expect(handleResponse).toBeCalledWith(searchRequests[1], { + id: searchRequests[1]._searchStrategyId, + }); }); test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { @@ -117,7 +100,7 @@ describe('callClient', () => { callClient(searchRequests, requestOptions, {} as FetchHandlers); abortController.abort(); - expect(mockAbortFns[0]).toBeCalled(); - expect(mockAbortFns[1]).not.toBeCalled(); + expect(mockAbortFn).toBeCalled(); + // expect(mockAbortFns[1]).not.toBeCalled(); }); }); diff --git a/src/plugins/data/public/search/fetch/call_client.ts b/src/plugins/data/public/search/fetch/call_client.ts index 6cc58b05ea183..b3c4c682fa60c 100644 --- a/src/plugins/data/public/search/fetch/call_client.ts +++ b/src/plugins/data/public/search/fetch/call_client.ts @@ -17,10 +17,9 @@ * under the License. */ -import { groupBy } from 'lodash'; import { handleResponse } from './handle_response'; import { FetchOptions, FetchHandlers } from './types'; -import { getSearchStrategyForSearchRequest, getSearchStrategyById } from '../search_strategy'; +import { defaultSearchStrategy } from '../search_strategy'; import { SearchRequest } from '..'; export function callClient( @@ -34,34 +33,18 @@ export function callClient( FetchOptions ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); const requestOptionsMap = new Map(requestOptionEntries); - - // Group the requests by the strategy used to search that specific request - const searchStrategyMap = groupBy(searchRequests, (request, i) => { - const searchStrategy = getSearchStrategyForSearchRequest(request, requestsOptions[i]); - return searchStrategy.id; - }); - - // Execute each search strategy with the group of requests, but return the responses in the same - // order in which they were received. We use a map to correlate the original request with its - // response. const requestResponseMap = new Map(); - Object.keys(searchStrategyMap).forEach(searchStrategyId => { - const searchStrategy = getSearchStrategyById(searchStrategyId); - const requests = searchStrategyMap[searchStrategyId]; - // There's no way `searchStrategy` could be undefined here because if we didn't get a matching strategy for this ID - // then an error would have been thrown above - const { searching, abort } = searchStrategy!.search({ - searchRequests: requests, - ...fetchHandlers, - }); + const { searching, abort } = defaultSearchStrategy.search({ + searchRequests, + ...fetchHandlers, + }); - requests.forEach((request, i) => { - const response = searching.then(results => handleResponse(request, results[i])); - const { abortSignal = null } = requestOptionsMap.get(request) || {}; - if (abortSignal) abortSignal.addEventListener('abort', abort); - requestResponseMap.set(request, response); - }); - }, []); + searchRequests.forEach((request, i) => { + const response = searching.then(results => handleResponse(request, results[i])); + const { abortSignal = null } = requestOptionsMap.get(request) || {}; + if (abortSignal) abortSignal.addEventListener('abort', abort); + requestResponseMap.set(request, response); + }); return searchRequests.map(request => requestResponseMap.get(request)); } diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 2a54cfe2be785..6ccd90c6a9eff 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -42,14 +42,7 @@ export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search export { LegacyApiCaller, SearchRequest, SearchResponse } from './es_client'; -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - defaultSearchStrategy, - SearchError, - SearchStrategyProvider, - getSearchErrorType, -} from './search_strategy'; +export { SearchError, SearchStrategyProvider, getSearchErrorType } from './search_strategy'; export { ISearchSource, diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 21e5ded6983ac..0c3321f03dabc 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -73,7 +73,7 @@ import _ from 'lodash'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; -import { SearchRequest } from '../..'; +import { IIndexPattern, SearchRequest } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { fetchSoon, FetchOptions, RequestFailure } from '../fetch'; @@ -339,11 +339,20 @@ export class SearchSource { return searchRequest; } + private getIndexType(index: IIndexPattern) { + if (this.searchStrategyId) { + return this.searchStrategyId === 'default' ? undefined : this.searchStrategyId; + } else { + return index?.type; + } + } + private flatten() { const searchRequest = this.mergeProps(); searchRequest.body = searchRequest.body || {}; const { body, index, fields, query, filters, highlightAll } = searchRequest; + searchRequest.indexType = this.getIndexType(index); const computedFields = index ? index.getComputedFields() : {}; diff --git a/src/plugins/data/public/search/search_strategy/default_search_strategy.ts b/src/plugins/data/public/search/search_strategy/default_search_strategy.ts index 6fcb1e6b3e8d2..2bd88f51587a8 100644 --- a/src/plugins/data/public/search/search_strategy/default_search_strategy.ts +++ b/src/plugins/data/public/search/search_strategy/default_search_strategy.ts @@ -74,7 +74,7 @@ function search({ }: SearchStrategySearchParams) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); - const promises = searchRequests.map(({ index, body }) => { + const promises = searchRequests.map(({ index, indexType, body }) => { const params = { index: index.title || index, body, @@ -82,7 +82,7 @@ function search({ }; const { signal } = abortController; return searchService - .search({ params }, { signal }) + .search({ params, indexType }, { signal }) .toPromise() .then(({ rawResponse }) => rawResponse); }); diff --git a/src/plugins/data/public/search/search_strategy/index.ts b/src/plugins/data/public/search/search_strategy/index.ts index 330e10d7d30e4..e3de2ea46e3ec 100644 --- a/src/plugins/data/public/search/search_strategy/index.ts +++ b/src/plugins/data/public/search/search_strategy/index.ts @@ -17,13 +17,6 @@ * under the License. */ -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - getSearchStrategyById, - getSearchStrategyForSearchRequest, -} from './search_strategy_registry'; - export { SearchError, getSearchErrorType } from './search_error'; export { SearchStrategyProvider, SearchStrategySearchParams } from './types'; diff --git a/src/plugins/data/public/search/search_strategy/search_strategy_registry.test.ts b/src/plugins/data/public/search/search_strategy/search_strategy_registry.test.ts deleted file mode 100644 index eaf86e1b270d5..0000000000000 --- a/src/plugins/data/public/search/search_strategy/search_strategy_registry.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IndexPattern } from '../..'; -import { noOpSearchStrategy } from './no_op_search_strategy'; -import { - searchStrategies, - addSearchStrategy, - getSearchStrategyByViability, - getSearchStrategyById, - getSearchStrategyForSearchRequest, - hasSearchStategyForIndexPattern, -} from './search_strategy_registry'; -import { SearchStrategyProvider } from './types'; - -const mockSearchStrategies: SearchStrategyProvider[] = [ - { - id: '0', - isViable: (index: IndexPattern) => index.id === '0', - search: () => ({ - searching: Promise.resolve([]), - abort: () => void 0, - }), - }, - { - id: '1', - isViable: (index: IndexPattern) => index.id === '1', - search: () => ({ - searching: Promise.resolve([]), - abort: () => void 0, - }), - }, -]; - -describe('Search strategy registry', () => { - beforeEach(() => { - searchStrategies.length = 0; - }); - - describe('addSearchStrategy', () => { - it('adds a search strategy', () => { - addSearchStrategy(mockSearchStrategies[0]); - expect(searchStrategies.length).toBe(1); - }); - - it('does not add a search strategy if it is already included', () => { - addSearchStrategy(mockSearchStrategies[0]); - addSearchStrategy(mockSearchStrategies[0]); - expect(searchStrategies.length).toBe(1); - }); - }); - - describe('getSearchStrategyByViability', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the viable strategy', () => { - expect(getSearchStrategyByViability({ id: '0' } as IndexPattern)).toBe( - mockSearchStrategies[0] - ); - expect(getSearchStrategyByViability({ id: '1' } as IndexPattern)).toBe( - mockSearchStrategies[1] - ); - }); - - it('returns undefined if there is no viable strategy', () => { - expect(getSearchStrategyByViability({ id: '-1' } as IndexPattern)).toBe(undefined); - }); - }); - - describe('getSearchStrategyById', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the strategy by ID', () => { - expect(getSearchStrategyById('0')).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyById('1')).toBe(mockSearchStrategies[1]); - }); - - it('returns undefined if there is no strategy with that ID', () => { - expect(getSearchStrategyById('-1')).toBe(undefined); - }); - - it('returns the noOp search strategy if passed that ID', () => { - expect(getSearchStrategyById('noOp')).toBe(noOpSearchStrategy); - }); - }); - - describe('getSearchStrategyForSearchRequest', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the strategy by ID if provided', () => { - expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: '1' })).toBe( - mockSearchStrategies[1] - ); - }); - - it('throws if there is no strategy by provided ID', () => { - expect(() => - getSearchStrategyForSearchRequest({}, { searchStrategyId: '-1' }) - ).toThrowErrorMatchingInlineSnapshot(`"No strategy with ID -1"`); - }); - - it('returns the strategy by viability if there is one', () => { - expect( - getSearchStrategyForSearchRequest({ - index: { - id: '1', - }, - }) - ).toBe(mockSearchStrategies[1]); - }); - - it('returns the no op strategy if there is no viable strategy', () => { - expect(getSearchStrategyForSearchRequest({ index: '3' })).toBe(noOpSearchStrategy); - }); - }); - - describe('hasSearchStategyForIndexPattern', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns whether there is a search strategy for this index pattern', () => { - expect(hasSearchStategyForIndexPattern({ id: '0' } as IndexPattern)).toBe(true); - expect(hasSearchStategyForIndexPattern({ id: '-1' } as IndexPattern)).toBe(false); - }); - }); -}); diff --git a/src/plugins/data/public/search/search_strategy/search_strategy_registry.ts b/src/plugins/data/public/search/search_strategy/search_strategy_registry.ts deleted file mode 100644 index 1ab6f7d4e1eff..0000000000000 --- a/src/plugins/data/public/search/search_strategy/search_strategy_registry.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IndexPattern } from '../..'; -import { SearchStrategyProvider } from './types'; -import { noOpSearchStrategy } from './no_op_search_strategy'; -import { SearchResponse } from '..'; - -export const searchStrategies: SearchStrategyProvider[] = []; - -export const addSearchStrategy = (searchStrategy: SearchStrategyProvider) => { - if (searchStrategies.includes(searchStrategy)) { - return; - } - - searchStrategies.push(searchStrategy); -}; - -export const getSearchStrategyByViability = (indexPattern: IndexPattern) => { - return searchStrategies.find(searchStrategy => { - return searchStrategy.isViable(indexPattern); - }); -}; - -export const getSearchStrategyById = (searchStrategyId: string) => { - return [...searchStrategies, noOpSearchStrategy].find(searchStrategy => { - return searchStrategy.id === searchStrategyId; - }); -}; - -export const getSearchStrategyForSearchRequest = ( - searchRequest: SearchResponse, - { searchStrategyId }: { searchStrategyId?: string } = {} -) => { - // Allow the searchSource to declare the correct strategy with which to execute its searches. - if (searchStrategyId != null) { - const strategy = getSearchStrategyById(searchStrategyId); - if (!strategy) throw Error(`No strategy with ID ${searchStrategyId}`); - return strategy; - } - - // Otherwise try to match it to a strategy. - const viableSearchStrategy = getSearchStrategyByViability(searchRequest.index); - - if (viableSearchStrategy) { - return viableSearchStrategy; - } - - // This search strategy automatically rejects with an error. - return noOpSearchStrategy; -}; - -export const hasSearchStategyForIndexPattern = (indexPattern: IndexPattern) => { - return Boolean(getSearchStrategyByViability(indexPattern)); -}; diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index 7183f14bdb255..36dcd4a00c05e 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -35,7 +35,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { sortBy, isEqual } from 'lodash'; -import { SavedQuery, SavedQueryAttributes, SavedQueryService } from '../..'; +import { SavedQuery, SavedQueryService } from '../..'; +import { SavedQueryAttributes } from '../../query'; interface Props { savedQuery?: SavedQueryAttributes; diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 26055a3ae41f7..b4ee02eefaf84 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -31,6 +31,13 @@ export const esSearchStrategyProvider: TSearchStrategyProvider { const config = await context.config$.pipe(first()).toPromise(); const defaultParams = getDefaultSearchParams(config); + + // Only default index pattern type is supported here. + // See data_enhanced for other type support. + if (!!request.indexType) { + throw new Error(`Unsupported index pattern type ${request.indexType}`); + } + const params = { ...defaultParams, ...request.params, diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 2b8c4b95ee022..e618f99084aed 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -43,11 +43,11 @@ export function registerSearchRoute(router: IRouter): void { return res.ok({ body: response }); } catch (err) { return res.customError({ - statusCode: err.statusCode, + statusCode: err.statusCode || 500, body: { message: err.message, attributes: { - error: err.body.error, + error: err.body?.error || err.message, }, }, }); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts b/src/setup_node_env/exit_on_warning.js similarity index 60% rename from src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts rename to src/setup_node_env/exit_on_warning.js index 272c8a4e19913..5be5ccd72bd02 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts +++ b/src/setup_node_env/exit_on_warning.js @@ -17,25 +17,22 @@ * under the License. */ -import sinon from 'sinon'; +if (process.noProcessWarnings !== true) { + var ignore = ['MaxListenersExceededWarning']; -import { State } from 'ui/state_management/state'; -import { Filter } from '../../../../../../../plugins/data/public'; + process.on('warning', function(warn) { + if (ignore.includes(warn.name)) return; -export class StubState implements State { - filters: Filter[]; - save: sinon.SinonSpy; + if (process.traceProcessWarnings === true) { + console.error('Node.js process-warning detected - Terminating process...'); + } else { + console.error('Node.js process-warning detected:'); + console.error(); + console.error(warn.stack); + console.error(); + console.error('Terminating process...'); + } - constructor() { - this.save = sinon.stub(); - this.filters = []; - } - - getQueryParamName() { - return '_a'; - } - - translateHashToRison(stateHashOrRison: string | string[]): string | string[] { - return ''; - } + process.exit(1); + }); } diff --git a/src/setup_node_env/index.js b/src/setup_node_env/index.js index 0f51f47572be6..97de5ba76b926 100644 --- a/src/setup_node_env/index.js +++ b/src/setup_node_env/index.js @@ -17,7 +17,11 @@ * under the License. */ -require('./harden'); // this require MUST be executed before any others +// The following require statements MUST be executed before any others - BEGIN +require('./exit_on_warning'); +require('./harden'); +// The following require statements MUST be executed before any others - END + require('symbol-observable'); require('./root'); require('./node_version_validator'); diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index e25d295515971..cf3d37d29b491 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -25,7 +25,13 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const inspector = getService('inspector'); + const docTable = getService('docTable'); const filterBar = getService('filterBar'); + const TEST_COLUMN_NAMES = ['@message']; + const TEST_FILTER_COLUMN_NAMES = [ + ['extension', 'jpg'], + ['geo.src', 'IN'], + ]; describe('Discover', () => { before(async () => { @@ -57,7 +63,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // skipping the test for new because we can't fix it right now it.skip('Click on new to clear the search', async () => { await PageObjects.discover.clickNewSearchButton(); await a11y.testAppSnapshot(); @@ -94,7 +99,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // unable to validate on EUI pop-over it('click share button', async () => { await PageObjects.share.clickShareTopNavButton(); await a11y.testAppSnapshot(); @@ -109,5 +113,29 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.closeSidebarFieldFilter(); await a11y.testAppSnapshot(); }); + + it('Add a field from sidebar', async () => { + for (const columnName of TEST_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItemAdd(columnName); + } + await a11y.testAppSnapshot(); + }); + + it.skip('Add more fields from sidebar', async () => { + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + } + await a11y.testAppSnapshot(); + }); + + // Context view test + it('should open context view on a doc', async () => { + await docTable.clickRowToggle(); + await (await docTable.getRowActions())[0].click(); + await a11y.testAppSnapshot(); + }); + + // Adding rest of the tests after https://github.com/elastic/kibana/issues/53888 is resolved }); } diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 3d9368f8d4680..55f6b56d9f0d1 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -25,7 +25,8 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); - describe('Index patterns on aliases', function() { + // FLAKY: https://github.com/elastic/kibana/issues/59717 + describe.skip('Index patterns on aliases', function() { before(async function() { await esArchiver.loadIfNeeded('alias'); await esArchiver.load('empty_kibana'); diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index 7759edbbf5bfc..0176424452d07 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -194,14 +194,6 @@ def getNextCommentMessage(previousCommentInfo = [:]) { .join("\n\n") } -def withGithubCredentials(closure) { - withCredentials([ - string(credentialsId: '2a9602aa-ab9f-4e52-baf3-b71ca88469c7', variable: 'GITHUB_TOKEN'), - ]) { - closure() - } -} - def postComment(message) { if (!isPr()) { error "Trying to post a GitHub PR comment on a non-PR or non-elastic PR build" diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 2b9b0eba38f46..cb5508642711a 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -202,12 +202,20 @@ def runErrorReporter() { } def call(Map params = [:], Closure closure) { - def config = [timeoutMinutes: 135] + params + def config = [timeoutMinutes: 135, checkPrChanges: false] + params stage("Kibana Pipeline") { timeout(time: config.timeoutMinutes, unit: 'MINUTES') { timestamps { ansiColor('xterm') { + if (config.checkPrChanges && githubPr.isPr()) { + print "Checking PR for changes to determine if CI needs to be run..." + + if (prChanges.areChangesSkippable()) { + print "No changes requiring CI found in PR, skipping." + return + } + } closure() } } @@ -215,4 +223,5 @@ def call(Map params = [:], Closure closure) { } } + return this diff --git a/vars/prChanges.groovy b/vars/prChanges.groovy new file mode 100644 index 0000000000000..a9eb9027a0597 --- /dev/null +++ b/vars/prChanges.groovy @@ -0,0 +1,52 @@ + +def getSkippablePaths() { + return [ + /^docs\//, + /^rfcs\//, + /^.ci\/.+\.yml$/, + /^\.github\//, + /\.md$/, + ] +} + +def areChangesSkippable() { + if (!githubPr.isPr()) { + return false + } + + try { + def skippablePaths = getSkippablePaths() + def files = getChangedFiles() + + // 3000 is the max files GH API will return + if (files.size() >= 3000) { + return false + } + + files = files.findAll { file -> + return !skippablePaths.find { regex -> file =~ regex} + } + + return files.size() < 1 + } catch (ex) { + buildUtils.printStacktrace(ex) + print "Error while checking to see if CI is skippable based on changes. Will run CI." + return false + } +} + +def getChanges() { + withGithubCredentials { + return githubPrs.getChanges(env.ghprbPullId) + } +} + +def getChangedFiles() { + def changes = getChanges() + def changedFiles = changes.collect { it.filename } + def renamedFiles = changes.collect { it.previousFilename }.findAll { it } + + return changedFiles + renamedFiles +} + +return this diff --git a/vars/withGithubCredentials.groovy b/vars/withGithubCredentials.groovy new file mode 100644 index 0000000000000..224e49af1bd6f --- /dev/null +++ b/vars/withGithubCredentials.groovy @@ -0,0 +1,9 @@ +def call(closure) { + withCredentials([ + string(credentialsId: '2a9602aa-ab9f-4e52-baf3-b71ca88469c7', variable: 'GITHUB_TOKEN'), + ]) { + closure() + } +} + +return this diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 53628ea970fb6..60a8d1fcbf229 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -26,7 +26,7 @@ "xpack.licensing": "plugins/licensing", "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", - "xpack.maps": "legacy/plugins/maps", + "xpack.maps": ["plugins/maps", "legacy/plugins/maps"], "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "plugins/remote_clusters", diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index c8d7cf29a3561..a456372c99c01 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -24,8 +24,7 @@ import { Document } from '../../persistence/saved_object_store'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; -import { SavedQuery } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { Filter, Query } from '../../../../../../../src/plugins/data/public'; +import { Filter, Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; export interface EditorFrameProps { doc?: Document; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index c669369e6e1d0..3066ac0e11325 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -206,6 +206,9 @@ const initialState: IndexPatternPrivateState = { }, }, }; + +const dslQuery = { bool: { must: [{ match_all: {} }], filter: [], should: [], must_not: [] } }; + describe('IndexPattern Data Panel', () => { let defaultProps: Parameters[0]; let core: ReturnType; @@ -271,8 +274,8 @@ describe('IndexPattern Data Panel', () => { describe('loading existence data', () => { function testProps() { const setState = jest.fn(); - core.http.get.mockImplementation(async ({ path }) => { - const parts = path.split('/'); + core.http.post.mockImplementation(async path => { + const parts = ((path as unknown) as string).split('/'); const indexPatternTitle = parts[parts.length - 1]; return { indexPatternTitle: `${indexPatternTitle}_testtitle`, @@ -385,24 +388,24 @@ describe('IndexPattern Data Panel', () => { }); expect(setState).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledTimes(2); + expect(core.http.post).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/a', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-01', timeFieldName: 'atime', - }, + }), }); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/a', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-02', timeFieldName: 'atime', - }, + }), }); const nextState = setState.mock.calls[1][0]({ @@ -428,22 +431,22 @@ describe('IndexPattern Data Panel', () => { expect(setState).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/a', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-01', timeFieldName: 'atime', - }, + }), }); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/b', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/b', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-01', timeFieldName: 'btime', - }, + }), }); const nextState = setState.mock.calls[1][0]({ @@ -476,13 +479,13 @@ describe('IndexPattern Data Panel', () => { let overlapCount = 0; const props = testProps(); - core.http.get.mockImplementation(({ path }) => { + core.http.post.mockImplementation(path => { if (queryCount) { ++overlapCount; } ++queryCount; - const parts = path.split('/'); + const parts = ((path as unknown) as string).split('/'); const indexPatternTitle = parts[parts.length - 1]; const result = Promise.resolve({ indexPatternTitle, @@ -516,7 +519,7 @@ describe('IndexPattern Data Panel', () => { inst.update(); }); - expect(core.http.get).toHaveBeenCalledTimes(2); + expect(core.http.post).toHaveBeenCalledTimes(2); expect(overlapCount).toEqual(0); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 1b86c07a31c11..7a3c04b67fbc4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -40,6 +40,7 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; +import { esQuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; export type Props = DatasourceDataPanelProps & { changeIndexPattern: ( @@ -113,6 +114,13 @@ export function IndexPatternDataPanel({ timeFieldName: indexPatterns[id].timeFieldName, })); + const dslQuery = esQuery.buildEsQuery( + indexPatterns[currentIndexPatternId] as IIndexPattern, + query, + filters, + esQuery.getEsQueryConfig(core.uiSettings) + ); + return ( <> `${x.title}:${x.timeFieldName}`).join(','), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts index f8961c30d14ee..ea9c8213ba909 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -535,9 +535,18 @@ describe('loader', () => { }); describe('syncExistingFields', () => { + const dslQuery = { + bool: { + must: [], + filter: [{ match_all: {} }], + should: [], + must_not: [], + }, + }; + it('should call once for each index pattern', async () => { const setState = jest.fn(); - const fetchJson = jest.fn(({ path }: { path: string }) => { + const fetchJson = jest.fn((path: string) => { const indexPatternTitle = _.last(path.split('/')); return { indexPatternTitle, @@ -553,6 +562,7 @@ describe('loader', () => { fetchJson: fetchJson as any, indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], setState, + dslQuery, }); expect(fetchJson).toHaveBeenCalledTimes(3); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts index ed3d8a91b366d..f4d5857f4826d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts @@ -215,26 +215,28 @@ export async function syncExistingFields({ dateRange, fetchJson, setState, + dslQuery, }: { dateRange: DateRange; indexPatterns: Array<{ id: string; timeFieldName?: string | null }>; - fetchJson: HttpSetup['get']; + fetchJson: HttpSetup['post']; setState: SetState; + dslQuery: object; }) { const emptinessInfo = await Promise.all( indexPatterns.map(pattern => { - const query: Record = { + const body: Record = { + dslQuery, fromDate: dateRange.fromDate, toDate: dateRange.toDate, }; if (pattern.timeFieldName) { - query.timeFieldName = pattern.timeFieldName; + body.timeFieldName = pattern.timeFieldName; } - return fetchJson({ - path: `${BASE_API_URL}/existing_fields/${pattern.id}`, - query, + return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { + body: JSON.stringify(body), }) as Promise; }) ); diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b62b920429e7c..b7983eeb8dbb8 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -7,12 +7,11 @@ import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'src/core/public'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; import { DragContextState } from './drag_drop'; import { Document } from './persistence'; import { DateRange } from '../../../../plugins/lens/common'; -import { Query, Filter } from '../../../../../src/plugins/data/public'; +import { Query, Filter, SavedQuery } from '../../../../../src/plugins/data/public'; // eslint-disable-next-line export interface EditorFrameOptions {} diff --git a/x-pack/legacy/plugins/maps/common/constants.ts b/x-pack/legacy/plugins/maps/common/constants.ts index a4afae0b9e077..98945653c25dc 100644 --- a/x-pack/legacy/plugins/maps/common/constants.ts +++ b/x-pack/legacy/plugins/maps/common/constants.ts @@ -3,173 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -export const EMS_CATALOGUE_PATH = 'ems/catalogue'; -export const EMS_FILES_CATALOGUE_PATH = 'ems/files'; -export const EMS_FILES_API_PATH = 'ems/files'; -export const EMS_FILES_DEFAULT_JSON_PATH = 'file'; -export const EMS_GLYPHS_PATH = 'fonts'; -export const EMS_SPRITES_PATH = 'sprites'; - -export const EMS_TILES_CATALOGUE_PATH = 'ems/tiles'; -export const EMS_TILES_API_PATH = 'ems/tiles'; -export const EMS_TILES_RASTER_STYLE_PATH = 'raster/style'; -export const EMS_TILES_RASTER_TILE_PATH = 'raster/tile'; - -export const EMS_TILES_VECTOR_STYLE_PATH = 'vector/style'; -export const EMS_TILES_VECTOR_SOURCE_PATH = 'vector/source'; -export const EMS_TILES_VECTOR_TILE_PATH = 'vector/tile'; - -export const MAP_SAVED_OBJECT_TYPE = 'map'; -export const APP_ID = 'maps'; -export const APP_ICON = 'gisApp'; -export const TELEMETRY_TYPE = 'maps-telemetry'; - -export const MAP_APP_PATH = `app/${APP_ID}`; -export const GIS_API_PATH = `api/${APP_ID}`; -export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; - -export const MAP_BASE_URL = `/${MAP_APP_PATH}#/${MAP_SAVED_OBJECT_TYPE}`; - -export function createMapPath(id: string) { - return `${MAP_BASE_URL}/${id}`; -} - -export const LAYER_TYPE = { - TILE: 'TILE', - VECTOR: 'VECTOR', - VECTOR_TILE: 'VECTOR_TILE', - HEATMAP: 'HEATMAP', -}; - -export enum SORT_ORDER { - ASC = 'asc', - DESC = 'desc', -} - -export const EMS_TMS = 'EMS_TMS'; -export const EMS_FILE = 'EMS_FILE'; -export const ES_GEO_GRID = 'ES_GEO_GRID'; -export const ES_SEARCH = 'ES_SEARCH'; -export const ES_PEW_PEW = 'ES_PEW_PEW'; -export const EMS_XYZ = 'EMS_XYZ'; // identifies a custom TMS source. Name is a little unfortunate. - -export enum FIELD_ORIGIN { - SOURCE = 'source', - JOIN = 'join', -} - -export const SOURCE_DATA_ID_ORIGIN = 'source'; -export const META_ID_ORIGIN_SUFFIX = 'meta'; -export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; -export const FORMATTERS_ID_ORIGIN_SUFFIX = 'formatters'; -export const SOURCE_FORMATTERS_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; - -export const GEOJSON_FILE = 'GEOJSON_FILE'; - -export const MIN_ZOOM = 0; -export const MAX_ZOOM = 24; - -export const DECIMAL_DEGREES_PRECISION = 5; // meters precision -export const ZOOM_PRECISION = 2; -export const DEFAULT_MAX_RESULT_WINDOW = 10000; -export const DEFAULT_MAX_INNER_RESULT_WINDOW = 100; -export const DEFAULT_MAX_BUCKETS_LIMIT = 10000; - -export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; -export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; - -export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; - -export const ES_GEO_FIELD_TYPE = { - GEO_POINT: 'geo_point', - GEO_SHAPE: 'geo_shape', -}; - -export const ES_SPATIAL_RELATIONS = { - INTERSECTS: 'INTERSECTS', - DISJOINT: 'DISJOINT', - WITHIN: 'WITHIN', -}; - -export const GEO_JSON_TYPE = { - POINT: 'Point', - MULTI_POINT: 'MultiPoint', - LINE_STRING: 'LineString', - MULTI_LINE_STRING: 'MultiLineString', - POLYGON: 'Polygon', - MULTI_POLYGON: 'MultiPolygon', - GEOMETRY_COLLECTION: 'GeometryCollection', -}; - -export const POLYGON_COORDINATES_EXTERIOR_INDEX = 0; -export const LON_INDEX = 0; -export const LAT_INDEX = 1; - -export const EMPTY_FEATURE_COLLECTION = { - type: 'FeatureCollection', - features: [], -}; - -export const DRAW_TYPE = { - BOUNDS: 'BOUNDS', - POLYGON: 'POLYGON', -}; - -export enum AGG_TYPE { - AVG = 'avg', - COUNT = 'count', - MAX = 'max', - MIN = 'min', - SUM = 'sum', - TERMS = 'terms', - UNIQUE_COUNT = 'cardinality', -} - -export enum RENDER_AS { - HEATMAP = 'heatmap', - POINT = 'point', - GRID = 'grid', -} - -export enum GRID_RESOLUTION { - COARSE = 'COARSE', - FINE = 'FINE', - MOST_FINE = 'MOST_FINE', -} - -export const TOP_TERM_PERCENTAGE_SUFFIX = '__percentage'; - -export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', { - defaultMessage: 'count', -}); - -export const COUNT_PROP_NAME = 'doc_count'; - -export const STYLE_TYPE = { - STATIC: 'STATIC', - DYNAMIC: 'DYNAMIC', -}; - -export const LAYER_STYLE_TYPE = { - VECTOR: 'VECTOR', - HEATMAP: 'HEATMAP', -}; - -export const COLOR_MAP_TYPE = { - CATEGORICAL: 'CATEGORICAL', - ORDINAL: 'ORDINAL', -}; - -export const COLOR_PALETTE_MAX_SIZE = 10; - -export const CATEGORICAL_DATA_TYPES = ['string', 'ip', 'boolean']; -export const ORDINAL_DATA_TYPES = ['number', 'date']; - -export const SYMBOLIZE_AS_TYPES = { - CIRCLE: 'circle', - ICON: 'icon', -}; - -export const DEFAULT_ICON = 'airfield'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export * from '../../../../plugins/maps/common/constants'; diff --git a/x-pack/legacy/plugins/maps/common/i18n_getters.ts b/x-pack/legacy/plugins/maps/common/i18n_getters.ts index 0008a119f1c7c..f9d186dea2e2b 100644 --- a/x-pack/legacy/plugins/maps/common/i18n_getters.ts +++ b/x-pack/legacy/plugins/maps/common/i18n_getters.ts @@ -4,49 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; - -import { $Values } from '@kbn/utility-types'; -import { ES_SPATIAL_RELATIONS } from './constants'; - -export function getAppTitle() { - return i18n.translate('xpack.maps.appTitle', { - defaultMessage: 'Maps', - }); -} - -export function getDataSourceLabel() { - return i18n.translate('xpack.maps.source.dataSourceLabel', { - defaultMessage: 'Data source', - }); -} - -export function getUrlLabel() { - return i18n.translate('xpack.maps.source.urlLabel', { - defaultMessage: 'Url', - }); -} - -export function getEsSpatialRelationLabel(spatialRelation: $Values) { - switch (spatialRelation) { - case ES_SPATIAL_RELATIONS.INTERSECTS: - return i18n.translate('xpack.maps.common.esSpatialRelation.intersectsLabel', { - defaultMessage: 'intersects', - }); - case ES_SPATIAL_RELATIONS.DISJOINT: - return i18n.translate('xpack.maps.common.esSpatialRelation.disjointLabel', { - defaultMessage: 'disjoint', - }); - case ES_SPATIAL_RELATIONS.WITHIN: - return i18n.translate('xpack.maps.common.esSpatialRelation.withinLabel', { - defaultMessage: 'within', - }); - // @ts-ignore - case ES_SPATIAL_RELATIONS.CONTAINS: - return i18n.translate('xpack.maps.common.esSpatialRelation.containsLabel', { - defaultMessage: 'contains', - }); - default: - return spatialRelation; - } -} +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export * from '../../../../plugins/maps/common/i18n_getters'; diff --git a/x-pack/legacy/plugins/maps/common/parse_xml_string.js b/x-pack/legacy/plugins/maps/common/parse_xml_string.js index 9d95e0e78280d..34ec144472828 100644 --- a/x-pack/legacy/plugins/maps/common/parse_xml_string.js +++ b/x-pack/legacy/plugins/maps/common/parse_xml_string.js @@ -4,19 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parseString } from 'xml2js'; - -// promise based wrapper around parseString -export async function parseXmlString(xmlString) { - const parsePromise = new Promise((resolve, reject) => { - parseString(xmlString, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - - return await parsePromise; -} +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export * from '../../../../plugins/maps/common/parse_xml_string'; diff --git a/x-pack/legacy/plugins/maps/common/style_property_descriptor_types.d.ts b/x-pack/legacy/plugins/maps/common/style_property_descriptor_types.d.ts new file mode 100644 index 0000000000000..8254055cf40b9 --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/style_property_descriptor_types.d.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { FIELD_ORIGIN, LABEL_BORDER_SIZES, SYMBOLIZE_AS_TYPES } from './constants'; + +// Non-static/dynamic options +export type SymbolizeAsOptions = { + value: SYMBOLIZE_AS_TYPES; +}; + +export type LabelBorderSizeOptions = { + size: LABEL_BORDER_SIZES; +}; + +// Static/dynamic options + +export type FieldMetaOptions = { + isEnabled: boolean; + sigma?: number; +}; + +export type StylePropertyField = { + name: string; + origin: FIELD_ORIGIN; +}; + +export type OrdinalColorStop = { + stop: number; + color: string; +}; + +export type CategoryColorStop = { + stop: string | null; + color: string; +}; + +export type IconStop = { + stop: string | null; + icon: string; +}; + +export type ColorDynamicOptions = { + // ordinal color properties + color: string; // TODO move color category ramps to constants and make ENUM type + customColorRamp?: OrdinalColorStop[]; + useCustomColorRamp?: boolean; + + // category color properties + colorCategory?: string; // TODO move color category palettes to constants and make ENUM type + customColorPalette?: CategoryColorStop[]; + useCustomColorPalette?: boolean; + + field?: StylePropertyField; + fieldMetaOptions: FieldMetaOptions; +}; + +export type ColorStaticOptions = { + color: string; +}; + +export type IconDynamicOptions = { + iconPaletteId?: string; + customIconStops?: IconStop[]; + useCustomIconMap?: boolean; + field?: StylePropertyField; + fieldMetaOptions: FieldMetaOptions; +}; + +export type IconStaticOptions = { + value: string; // icon id +}; + +export type LabelDynamicOptions = { + field: StylePropertyField; // field containing label value +}; + +export type LabelStaticOptions = { + value: string; // static label text +}; + +export type OrientationDynamicOptions = { + field?: StylePropertyField; + fieldMetaOptions: FieldMetaOptions; +}; + +export type OrientationStaticOptions = { + orientation: number; +}; + +export type SizeDynamicOptions = { + minSize: number; + maxSize: number; + field?: StylePropertyField; + fieldMetaOptions: FieldMetaOptions; +}; + +export type SizeStaticOptions = { + size: number; +}; + +export type StylePropertyOptions = + | LabelBorderSizeOptions + | SymbolizeAsOptions + | DynamicStylePropertyOptions + | StaticStylePropertyOptions; + +export type StaticStylePropertyOptions = + | ColorStaticOptions + | IconStaticOptions + | LabelStaticOptions + | OrientationStaticOptions + | SizeStaticOptions; + +export type DynamicStylePropertyOptions = + | ColorDynamicOptions + | IconDynamicOptions + | LabelDynamicOptions + | OrientationDynamicOptions + | SizeDynamicOptions; diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 8048c21fe9333..1a7f478d3bbad 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -52,7 +52,6 @@ export function maps(kibana) { }; }, embeddableFactories: ['plugins/maps/embeddable/map_embeddable_factory'], - inspectorViews: ['plugins/maps/inspector/views/register_views'], home: ['plugins/maps/legacy_register_feature'], styleSheetPaths: `${__dirname}/public/index.scss`, savedObjectSchemas: { diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index cfca044ea759a..7a1e5e5266246 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -20,13 +20,15 @@ import { getQuery, getDataRequestDescriptor, } from '../selectors/map_selectors'; -import { FLYOUT_STATE } from '../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE } from '../../../../../plugins/maps/public/reducers/ui'; import { cancelRequest, registerCancelCallback, unregisterCancelCallback, getEventHandlers, -} from '../reducers/non_serializable_instances'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; import { updateFlyout } from '../actions/ui_actions'; import { FEATURE_ID_PROPERTY_NAME, @@ -34,48 +36,52 @@ import { SOURCE_DATA_ID_ORIGIN, } from '../../common/constants'; -export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER'; -export const SET_TRANSIENT_LAYER = 'SET_TRANSIENT_LAYER'; -export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER'; -export const ADD_LAYER = 'ADD_LAYER'; -export const SET_LAYER_ERROR_STATUS = 'SET_LAYER_ERROR_STATUS'; -export const ADD_WAITING_FOR_MAP_READY_LAYER = 'ADD_WAITING_FOR_MAP_READY_LAYER'; -export const CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST = 'CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST'; -export const REMOVE_LAYER = 'REMOVE_LAYER'; -export const SET_LAYER_VISIBILITY = 'SET_LAYER_VISIBILITY'; -export const MAP_EXTENT_CHANGED = 'MAP_EXTENT_CHANGED'; -export const MAP_READY = 'MAP_READY'; -export const MAP_DESTROYED = 'MAP_DESTROYED'; -export const LAYER_DATA_LOAD_STARTED = 'LAYER_DATA_LOAD_STARTED'; -export const LAYER_DATA_LOAD_ENDED = 'LAYER_DATA_LOAD_ENDED'; -export const LAYER_DATA_LOAD_ERROR = 'LAYER_DATA_LOAD_ERROR'; -export const UPDATE_SOURCE_DATA_REQUEST = 'UPDATE_SOURCE_DATA_REQUEST'; -export const SET_JOINS = 'SET_JOINS'; -export const SET_QUERY = 'SET_QUERY'; -export const TRIGGER_REFRESH_TIMER = 'TRIGGER_REFRESH_TIMER'; -export const UPDATE_LAYER_PROP = 'UPDATE_LAYER_PROP'; -export const UPDATE_LAYER_STYLE = 'UPDATE_LAYER_STYLE'; -export const SET_LAYER_STYLE_META = 'SET_LAYER_STYLE_META'; -export const TOUCH_LAYER = 'TOUCH_LAYER'; -export const UPDATE_SOURCE_PROP = 'UPDATE_SOURCE_PROP'; -export const SET_REFRESH_CONFIG = 'SET_REFRESH_CONFIG'; -export const SET_MOUSE_COORDINATES = 'SET_MOUSE_COORDINATES'; -export const CLEAR_MOUSE_COORDINATES = 'CLEAR_MOUSE_COORDINATES'; -export const SET_GOTO = 'SET_GOTO'; -export const CLEAR_GOTO = 'CLEAR_GOTO'; -export const TRACK_CURRENT_LAYER_STATE = 'TRACK_CURRENT_LAYER_STATE'; -export const ROLLBACK_TO_TRACKED_LAYER_STATE = 'ROLLBACK_TO_TRACKED_LAYER_STATE'; -export const REMOVE_TRACKED_LAYER_STATE = 'REMOVE_TRACKED_LAYER_STATE'; -export const SET_OPEN_TOOLTIPS = 'SET_OPEN_TOOLTIPS'; -export const UPDATE_DRAW_STATE = 'UPDATE_DRAW_STATE'; -export const SET_SCROLL_ZOOM = 'SET_SCROLL_ZOOM'; -export const SET_MAP_INIT_ERROR = 'SET_MAP_INIT_ERROR'; -export const SET_INTERACTIVE = 'SET_INTERACTIVE'; -export const DISABLE_TOOLTIP_CONTROL = 'DISABLE_TOOLTIP_CONTROL'; -export const HIDE_TOOLBAR_OVERLAY = 'HIDE_TOOLBAR_OVERLAY'; -export const HIDE_LAYER_CONTROL = 'HIDE_LAYER_CONTROL'; -export const HIDE_VIEW_CONTROL = 'HIDE_VIEW_CONTROL'; -export const SET_WAITING_FOR_READY_HIDDEN_LAYERS = 'SET_WAITING_FOR_READY_HIDDEN_LAYERS'; +import { + SET_SELECTED_LAYER, + SET_TRANSIENT_LAYER, + UPDATE_LAYER_ORDER, + ADD_LAYER, + SET_LAYER_ERROR_STATUS, + ADD_WAITING_FOR_MAP_READY_LAYER, + CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, + REMOVE_LAYER, + SET_LAYER_VISIBILITY, + MAP_EXTENT_CHANGED, + MAP_READY, + MAP_DESTROYED, + LAYER_DATA_LOAD_STARTED, + LAYER_DATA_LOAD_ENDED, + LAYER_DATA_LOAD_ERROR, + UPDATE_SOURCE_DATA_REQUEST, + SET_JOINS, + SET_QUERY, + TRIGGER_REFRESH_TIMER, + UPDATE_LAYER_PROP, + UPDATE_LAYER_STYLE, + SET_LAYER_STYLE_META, + UPDATE_SOURCE_PROP, + SET_REFRESH_CONFIG, + SET_MOUSE_COORDINATES, + CLEAR_MOUSE_COORDINATES, + SET_GOTO, + CLEAR_GOTO, + TRACK_CURRENT_LAYER_STATE, + ROLLBACK_TO_TRACKED_LAYER_STATE, + REMOVE_TRACKED_LAYER_STATE, + SET_OPEN_TOOLTIPS, + UPDATE_DRAW_STATE, + SET_SCROLL_ZOOM, + SET_MAP_INIT_ERROR, + SET_INTERACTIVE, + DISABLE_TOOLTIP_CONTROL, + HIDE_TOOLBAR_OVERLAY, + HIDE_LAYER_CONTROL, + HIDE_VIEW_CONTROL, + SET_WAITING_FOR_READY_HIDDEN_LAYERS, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/actions/map_actions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export * from '../../../../../plugins/maps/public/actions/map_actions'; function getLayerLoadingCallbacks(dispatch, getState, layerId) { return { diff --git a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js b/x-pack/legacy/plugins/maps/public/actions/ui_actions.js index 2b687516f3e5a..33ab2fd74122a 100644 --- a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/ui_actions.js @@ -4,16 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -export const UPDATE_FLYOUT = 'UPDATE_FLYOUT'; -export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW'; -export const OPEN_SET_VIEW = 'OPEN_SET_VIEW'; -export const SET_IS_LAYER_TOC_OPEN = 'SET_IS_LAYER_TOC_OPEN'; -export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; -export const SET_READ_ONLY = 'SET_READ_ONLY'; -export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; -export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; -export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; -export const UPDATE_INDEXING_STAGE = 'UPDATE_INDEXING_STAGE'; +import { + UPDATE_FLYOUT, + CLOSE_SET_VIEW, + OPEN_SET_VIEW, + SET_IS_LAYER_TOC_OPEN, + SET_FULL_SCREEN, + SET_READ_ONLY, + SET_OPEN_TOC_DETAILS, + SHOW_TOC_DETAILS, + HIDE_TOC_DETAILS, + UPDATE_INDEXING_STAGE, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/actions/ui_actions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export * from '../../../../../plugins/maps/public/actions/ui_actions'; + +export function exitFullScreen() { + return { + type: SET_FULL_SCREEN, + isFullScreen: false, + }; +} export function updateFlyout(display) { return { @@ -37,12 +49,6 @@ export function setIsLayerTOCOpen(isLayerTOCOpen) { isLayerTOCOpen, }; } -export function exitFullScreen() { - return { - type: SET_FULL_SCREEN, - isFullScreen: false, - }; -} export function enableFullScreen() { return { type: SET_FULL_SCREEN, diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 84ead42d3374e..7b3dc74d777b2 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -17,7 +17,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { uiModules } from 'ui/modules'; import { timefilter } from 'ui/timefilter'; import { Provider } from 'react-redux'; -import { createMapStore } from '../reducers/store'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; import { GisMap } from '../connected_components/gis_map'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { @@ -28,7 +29,11 @@ import { setQuery, clearTransientLayerStateAndCloseFlyout, } from '../actions/map_actions'; -import { DEFAULT_IS_LAYER_TOC_OPEN, FLYOUT_STATE } from '../reducers/ui'; +import { + DEFAULT_IS_LAYER_TOC_OPEN, + FLYOUT_STATE, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/reducers/ui'; import { enableFullScreen, updateFlyout, @@ -37,13 +42,15 @@ import { setOpenTOCDetails, } from '../actions/ui_actions'; import { getIsFullScreen } from '../selectors/ui_selectors'; -import { copyPersistentState } from '../reducers/util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../../../../../plugins/maps/public/reducers/util'; import { getQueryableUniqueIndexPatternIds, hasDirtyState, getLayerListRaw, } from '../selectors/map_selectors'; -import { getInspectorAdapters } from '../reducers/non_serializable_instances'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; import { docTitle } from 'ui/doc_title'; import { indexPatternService, getInspector } from '../kibana_services'; import { toastNotifications } from 'ui/notify'; diff --git a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js index 490ab16a1799c..f846d3d4a617f 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js @@ -18,7 +18,8 @@ import { } from '../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors'; import { convertMapExtentToPolygon } from '../../elasticsearch_geo_utils'; -import { copyPersistentState } from '../../reducers/util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../../../../../../plugins/maps/public/reducers/util'; import { extractReferences, injectReferences } from '../../../common/migrations/references'; import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js index ceb0a6ea9f922..39cb2c469e054 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js @@ -6,7 +6,8 @@ import { connect } from 'react-redux'; import { GisMap } from './view'; -import { FLYOUT_STATE } from '../../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE } from '../../../../../../plugins/maps/public/reducers/ui'; import { exitFullScreen } from '../../actions/ui_actions'; import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js index 8d0dd0c266f28..e8192795f98ae 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js @@ -6,8 +6,10 @@ import { connect } from 'react-redux'; import { ImportEditor } from './view'; -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; -import { INDEXING_STAGE } from '../../../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { INDEXING_STAGE } from '../../../../../../../plugins/maps/public/reducers/ui'; import { updateIndexingStage } from '../../../actions/ui_actions'; import { getIndexingStage } from '../../../selectors/ui_selectors'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js index d2b43775c5a49..c4e2fa5169b0f 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js @@ -6,11 +6,13 @@ import { connect } from 'react-redux'; import { AddLayerPanel } from './view'; -import { FLYOUT_STATE, INDEXING_STAGE } from '../../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE, INDEXING_STAGE } from '../../../../../../plugins/maps/public/reducers/ui'; import { updateFlyout, updateIndexingStage } from '../../actions/ui_actions'; import { getFlyoutDisplay, getIndexingStage } from '../../selectors/ui_selectors'; import { getMapColors } from '../../selectors/map_selectors'; -import { getInspectorAdapters } from '../../reducers/non_serializable_instances'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInspectorAdapters } from '../../../../../../plugins/maps/public/reducers/non_serializable_instances'; import { setTransientLayer, addLayer, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js index 51ed19d1c77d1..553e54ee89766 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js @@ -6,7 +6,8 @@ import { connect } from 'react-redux'; import { SourceEditor } from './view'; -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; function mapStateToProps(state = {}) { return { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js index 76e650cad97eb..287f0019f18ec 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js @@ -6,7 +6,8 @@ import { connect } from 'react-redux'; import { FlyoutFooter } from './view'; -import { FLYOUT_STATE } from '../../../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui'; import { updateFlyout } from '../../../actions/ui_actions'; import { hasDirtyState } from '../../../selectors/map_selectors'; import { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js index a2f121a9377fe..350cb7028abee 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js @@ -24,7 +24,8 @@ import { isTooltipControlDisabled, isViewControlHidden, } from '../../../selectors/map_selectors'; -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; function mapStateToProps(state = {}) { return { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js index 0b090a639edb2..e51e59ec41e18 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js @@ -6,7 +6,8 @@ import { connect } from 'react-redux'; import { LayerControl } from './view'; -import { FLYOUT_STATE } from '../../../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui.js'; import { updateFlyout, setIsLayerTOCOpen } from '../../../actions/ui_actions'; import { setSelectedLayer } from '../../../actions/map_actions'; import { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js index e9debdba7b914..ececc5a90ab89 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js @@ -7,7 +7,8 @@ import _ from 'lodash'; import { connect } from 'react-redux'; import { TOCEntry } from './view'; -import { FLYOUT_STATE } from '../../../../../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE } from '../../../../../../../../../plugins/maps/public/reducers/ui.js'; import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions'; import { getIsReadOnly, getOpenTOCDetails } from '../../../../../selectors/ui_selectors'; import { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js index 5988a128232d6..650e827cc1656 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js @@ -19,7 +19,8 @@ import { esFilters } from '../../../../../../src/plugins/data/public'; import { I18nContext } from 'ui/i18n'; import { GisMap } from '../connected_components/gis_map'; -import { createMapStore } from '../reducers/store'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; import { npStart } from 'ui/new_platform'; import { setGotoWithCenter, @@ -36,7 +37,11 @@ import { } from '../actions/map_actions'; import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; -import { getInspectorAdapters, setEventHandlers } from '../reducers/non_serializable_instances'; +import { + getInspectorAdapters, + setEventHandlers, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; import { getMapCenter, getMapZoom, getHiddenLayerIds } from '../selectors/map_selectors'; import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js index 73f222615493b..710b7f737e861 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js @@ -17,7 +17,8 @@ import { MapEmbeddable } from './map_embeddable'; import { indexPatternService } from '../kibana_services'; import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; -import { createMapStore } from '../reducers/store'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; import { addLayerWithoutDataSync } from '../actions/map_actions'; import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; import { getInitialLayers } from '../angular/get_initial_layers'; diff --git a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js b/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js index 935747da93687..8e3e0a9168e30 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js @@ -5,7 +5,8 @@ */ import _ from 'lodash'; -import { DEFAULT_IS_LAYER_TOC_OPEN } from '../reducers/ui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../../../plugins/maps/public/reducers/ui'; const MAP_EMBEDDABLE_INPUT_KEYS = [ 'hideFilterActions', diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 71e5d7b95e44f..5c9532a3841f3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -14,7 +14,8 @@ import { SOURCE_DATA_ID_ORIGIN, } from '../../common/constants'; import uuid from 'uuid/v4'; -import { copyPersistentState } from '../reducers/util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../../../../../plugins/maps/public/reducers/util.js'; import { i18n } from '@kbn/i18n'; export class AbstractLayer { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index f575fd05c8061..1552db277e609 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -16,7 +16,8 @@ import { timefilter } from 'ui/timefilter'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; -import { copyPersistentState } from '../../reducers/util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../../../../../../plugins/maps/public/reducers/util'; import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; import { DataRequestAbortError } from '../util/data_request'; import { expandToTileBoundaries } from './es_geo_grid_source/geo_tile_utils'; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index 4fef52e731f9b..b6b6c10831bb5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { copyPersistentState } from '../../reducers/util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../../../../../../plugins/maps/public/reducers/util'; export class AbstractSource { static isIndexingSource = false; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts index f6f4dff88bdda..5fa8b28aa837b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts @@ -28,6 +28,7 @@ export interface IVectorSource extends ISource { ): Promise; getFields(): Promise; + getFieldByName(fieldName: string): IField; } export class AbstractVectorSource extends AbstractSource { @@ -38,4 +39,5 @@ export class AbstractVectorSource extends AbstractSource { ): Promise; getFields(): Promise; + getFieldByName(fieldName: string): IField; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/categorical_field_meta_popover.tsx b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/categorical_field_meta_popover.tsx new file mode 100644 index 0000000000000..9aec7ece45f36 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/categorical_field_meta_popover.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import _ from 'lodash'; +import React from 'react'; +import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FieldMetaPopover } from './field_meta_popover'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; +import { FieldMetaOptions } from '../../../../../../common/style_property_descriptor_types'; + +type Props = { + styleProperty: IDynamicStyleProperty; + onChange: (fieldMetaOptions: FieldMetaOptions) => void; +}; + +export function CategoricalFieldMetaPopover(props: Props) { + const onIsEnabledChange = (event: EuiSwitchEvent) => { + props.onChange({ + ...props.styleProperty.getFieldMetaOptions(), + isEnabled: event.target.checked, + }); + }; + + return ( + + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/field_meta_popover.tsx b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/field_meta_popover.tsx new file mode 100644 index 0000000000000..dfd98937135e1 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/field_meta_popover.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { Component, ReactElement } from 'react'; +import { EuiButtonIcon, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +type Props = { + children: ReactElement; +}; + +type State = { + isPopoverOpen: boolean; +}; + +export class FieldMetaPopover extends Component { + state = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + _renderButton() { + return ( + + ); + } + + render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx new file mode 100644 index 0000000000000..0980f7df74e3c --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import _ from 'lodash'; +import React, { ChangeEvent, Fragment, MouseEvent } from 'react'; +import { EuiFormRow, EuiRange, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { DEFAULT_SIGMA, VECTOR_STYLES } from '../../vector_style_defaults'; +import { FieldMetaPopover } from './field_meta_popover'; +import { IDynamicStyleProperty } from '../../properties/dynamic_style_property'; +import { FieldMetaOptions } from '../../../../../../common/style_property_descriptor_types'; + +function getIsEnableToggleLabel(styleName: string) { + switch (styleName) { + case VECTOR_STYLES.FILL_COLOR: + case VECTOR_STYLES.LINE_COLOR: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel', { + defaultMessage: 'Calculate color ramp range from indices', + }); + case VECTOR_STYLES.LINE_WIDTH: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel', { + defaultMessage: 'Calculate border width range from indices', + }); + case VECTOR_STYLES.ICON_SIZE: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel', { + defaultMessage: 'Calculate symbol size range from indices', + }); + default: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel', { + defaultMessage: 'Calculate symbolization range from indices', + }); + } +} + +type Props = { + styleProperty: IDynamicStyleProperty; + onChange: (fieldMetaOptions: FieldMetaOptions) => void; +}; + +export function OrdinalFieldMetaPopover(props: Props) { + const onIsEnabledChange = (event: EuiSwitchEvent) => { + props.onChange({ + ...props.styleProperty.getFieldMetaOptions(), + isEnabled: event.target.checked, + }); + }; + + const onSigmaChange = (event: ChangeEvent | MouseEvent) => { + props.onChange({ + ...props.styleProperty.getFieldMetaOptions(), + sigma: parseInt(event.currentTarget.value, 10), + }); + }; + + return ( + + + + + + + + + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js index 04bb800eb1ecf..a65065bbb2032 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js @@ -7,9 +7,10 @@ import React from 'react'; import { EuiFormRow, EuiSelect, EuiToolTip } from '@elastic/eui'; -import { LABEL_BORDER_SIZES, VECTOR_STYLES } from '../../vector_style_defaults'; +import { VECTOR_STYLES } from '../../vector_style_defaults'; import { getVectorStyleLabel, getDisabledByMessage } from '../get_vector_style_label'; import { i18n } from '@kbn/i18n'; +import { LABEL_BORDER_SIZES } from '../../../../../../common/constants'; const options = [ { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js deleted file mode 100644 index dee333f163960..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component, Fragment } from 'react'; -import { EuiButtonIcon, EuiFormRow, EuiPopover, EuiRange, EuiSwitch } from '@elastic/eui'; -import { VECTOR_STYLES } from '../vector_style_defaults'; -import { i18n } from '@kbn/i18n'; - -function getIsEnableToggleLabel(styleName) { - switch (styleName) { - case VECTOR_STYLES.FILL_COLOR: - case VECTOR_STYLES.LINE_COLOR: - return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel', { - defaultMessage: 'Calculate color ramp range from indices', - }); - case VECTOR_STYLES.LINE_WIDTH: - return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel', { - defaultMessage: 'Calculate border width range from indices', - }); - case VECTOR_STYLES.ICON_SIZE: - return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel', { - defaultMessage: 'Calculate symbol size range from indices', - }); - default: - return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel', { - defaultMessage: 'Calculate symbolization range from indices', - }); - } -} - -export class OrdinalFieldMetaOptionsPopover extends Component { - state = { - isPopoverOpen: false, - }; - - _togglePopover = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - }; - - _closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; - - _onIsEnabledChange = event => { - this.props.onChange({ - ...this.props.styleProperty.getFieldMetaOptions(), - isEnabled: event.target.checked, - }); - }; - - _onSigmaChange = event => { - this.props.onChange({ - ...this.props.styleProperty.getFieldMetaOptions(), - sigma: event.target.value, - }); - }; - - _renderButton() { - return ( - - ); - } - - _renderContent() { - return ( - - - - - - - - - - ); - } - - render() { - if (!this.props.styleProperty.supportsFieldMeta()) { - return null; - } - - return ( - - {this._renderContent()} - - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 7ad36bd2ae33d..8e05cf287efa6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -18,7 +18,6 @@ import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties, - LABEL_BORDER_SIZES, VECTOR_STYLES, } from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils'; @@ -26,7 +25,11 @@ import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; -import { CATEGORICAL_DATA_TYPES, ORDINAL_DATA_TYPES } from '../../../../../common/constants'; +import { + CATEGORICAL_DATA_TYPES, + ORDINAL_DATA_TYPES, + LABEL_BORDER_SIZES, +} from '../../../../../common/constants'; export class VectorStyleEditor extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 9404c2da3d274..417426f12fc98 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -84,7 +84,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { return this._options.useCustomColorRamp; } - supportsFeatureState() { + supportsMbFeatureState() { return true; } @@ -101,34 +101,26 @@ export class DynamicColorProperty extends DynamicStyleProperty { } _getMbColor() { - const isDynamicConfigComplete = - _.has(this._options, 'field.name') && _.has(this._options, 'color'); - if (!isDynamicConfigComplete) { + if (!_.get(this._options, 'field.name')) { return null; } - const targetName = getComputedFieldName(this._styleName, this._options.field.name); - if (this.isCategorical()) { - return this._getMbDataDrivenCategoricalColor({ targetName }); - } else { - return this._getMbDataDrivenOrdinalColor({ targetName }); - } + return this.isCategorical() + ? this._getCategoricalColorMbExpression() + : this._getOrdinalColorMbExpression(); } - _getMbDataDrivenOrdinalColor({ targetName }) { - if ( - this._options.useCustomColorRamp && - (!this._options.customColorRamp || !this._options.customColorRamp.length) - ) { - return null; - } - - const colorStops = this._getMbOrdinalColorStops(); - if (!colorStops) { - return null; - } - + _getOrdinalColorMbExpression() { + const targetName = getComputedFieldName(this._styleName, this._options.field.name); if (this._options.useCustomColorRamp) { + if (!this._options.customColorRamp || !this._options.customColorRamp.length) { + // custom color ramp config is not complete + return null; + } + + const colorStops = this._options.customColorRamp.reduce((accumulatedStops, nextStop) => { + return [...accumulatedStops, nextStop.stop, nextStop.color]; + }, []); const firstStopValue = colorStops[0]; const lessThenFirstStopValue = firstStopValue - 1; return [ @@ -138,6 +130,11 @@ export class DynamicColorProperty extends DynamicStyleProperty { ...colorStops, ]; } + + const colorStops = getOrdinalColorRampStops(this._options.color); + if (!colorStops) { + return null; + } return [ 'interpolate', ['linear'], @@ -194,7 +191,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { }; } - _getMbDataDrivenCategoricalColor() { + _getCategoricalColorMbExpression() { if ( this._options.useCustomColorPalette && (!this._options.customColorPalette || !this._options.customColorPalette.length) @@ -226,16 +223,6 @@ export class DynamicColorProperty extends DynamicStyleProperty { return ['match', ['to-string', ['get', this._options.field.name]], ...mbStops]; } - _getMbOrdinalColorStops() { - if (this._options.useCustomColorRamp) { - return this._options.customColorRamp.reduce((accumulatedStops, nextStop) => { - return [...accumulatedStops, nextStop.stop, nextStop.color]; - }, []); - } else { - return getOrdinalColorRampStops(this._options.color); - } - } - renderRangeLegendHeader() { if (this._options.color) { return ; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js index 5b286e4ba120e..f74deb17fff7c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js @@ -237,6 +237,226 @@ test('Should pluck the categorical style-meta from fieldmeta', async () => { }); }); +describe('get mapbox color expression', () => { + describe('ordinal color ramp', () => { + test('should return null when field is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return null when field name is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + field: {}, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + describe('pre-defined color ramp', () => { + test('should return null when color ramp is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'myField', + }, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return mapbox expression for color ramp', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'myField', + }, + color: 'Blues', + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toEqual([ + 'interpolate', + ['linear'], + ['coalesce', ['feature-state', '__kbn__dynamic__myField__lineColor'], -1], + -1, + 'rgba(0,0,0,0)', + 0, + '#f7faff', + 0.125, + '#ddeaf7', + 0.25, + '#c5daee', + 0.375, + '#9dc9e0', + 0.5, + '#6aadd5', + 0.625, + '#4191c5', + 0.75, + '#2070b4', + 0.875, + '#072f6b', + ]); + }); + }); + + describe('custom color ramp', () => { + test('should return null when customColorRamp is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'myField', + }, + useCustomColorRamp: true, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return null when customColorRamp is empty', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'myField', + }, + useCustomColorRamp: true, + customColorRamp: [], + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return mapbox expression for custom color ramp', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'myField', + }, + useCustomColorRamp: true, + customColorRamp: [ + { stop: 10, color: '#f7faff' }, + { stop: 100, color: '#072f6b' }, + ], + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toEqual([ + 'step', + ['coalesce', ['feature-state', '__kbn__dynamic__myField__lineColor'], 9], + 'rgba(0,0,0,0)', + 10, + '#f7faff', + 100, + '#072f6b', + ]); + }); + }); + }); + + describe('categorical color palette', () => { + test('should return null when field is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return null when field name is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + field: {}, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + describe('pre-defined color palette', () => { + test('should return null when color palette is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + field: { + name: 'myField', + }, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return mapbox expression for color palette', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + field: { + name: 'myField', + }, + colorCategory: 'palette_0', + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toEqual([ + 'match', + ['to-string', ['get', 'myField']], + 'US', + '#54B399', + 'CN', + '#6092C0', + '#D36086', + ]); + }); + }); + + describe('custom color palette', () => { + test('should return null when customColorPalette is not provided', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + field: { + name: 'myField', + }, + useCustomColorPalette: true, + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return null when customColorPalette is empty', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + field: { + name: 'myField', + }, + useCustomColorPalette: true, + customColorPalette: [], + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toBeNull(); + }); + + test('should return mapbox expression for custom color palette', async () => { + const dynamicStyleOptions = { + type: COLOR_MAP_TYPE.CATEGORICAL, + field: { + name: 'myField', + }, + useCustomColorPalette: true, + customColorPalette: [ + { stop: null, color: '#f7faff' }, + { stop: 'MX', color: '#072f6b' }, + ], + }; + const colorProperty = makeProperty(dynamicStyleOptions); + expect(colorProperty._getMbColor()).toEqual([ + 'match', + ['to-string', ['get', 'myField']], + 'MX', + '#072f6b', + '#f7faff', + ]); + }); + }); + }); +}); + test('isCategorical should return true when type is categorical', async () => { const categoricalColorStyle = makeProperty({ type: COLOR_MAP_TYPE.CATEGORICAL, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index 1d2457142c082..81b476b717c94 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -22,7 +22,7 @@ export class DynamicOrientationProperty extends DynamicStyleProperty { } } - supportsFeatureState() { + supportsMbFeatureState() { return false; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index 77f2d09982291..97bb252b3da1d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -48,7 +48,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { this._isSymbolizedAsIcon = isSymbolizedAsIcon; } - supportsFeatureState() { + supportsMbFeatureState() { // mb style "icon-size" does not support feature state if (this.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._isSymbolizedAsIcon) { return false; @@ -124,7 +124,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _getMbDataDrivenSize({ targetName, minSize, maxSize }) { - const lookup = this.supportsFeatureState() ? 'feature-state' : 'get'; + const lookup = this.supportsMbFeatureState() ? 'feature-state' : 'get'; return [ 'interpolate', ['linear'], diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts new file mode 100644 index 0000000000000..f4c487b28757e --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { IStyleProperty } from './style_property'; +import { FIELD_ORIGIN } from '../../../../../common/constants'; +import { FieldMetaOptions } from '../../../../../common/style_property_descriptor_types'; +import { IField } from '../../../fields/field'; +import { IVectorLayer } from '../../../vector_layer'; +import { IVectorSource } from '../../../sources/vector_source'; +import { CategoryFieldMeta, RangeFieldMeta } from '../../../../../common/descriptor_types'; + +export interface IDynamicStyleProperty extends IStyleProperty { + getFieldMetaOptions(): FieldMetaOptions; + getField(): IField | undefined; + getFieldName(): string; + getFieldOrigin(): FIELD_ORIGIN | undefined; + getComputedFieldName(): string | undefined; + getRangeFieldMeta(): RangeFieldMeta; + getCategoryFieldMeta(): CategoryFieldMeta; + isFieldMetaEnabled(): boolean; + supportsFieldMeta(): boolean; + getFieldMetaRequest(): Promise; + supportsMbFeatureState(): boolean; + pluckOrdinalStyleMetaFromFeatures(features: unknown[]): RangeFieldMeta; + pluckCategoricalStyleMetaFromFeatures(features: unknown[]): CategoryFieldMeta; + pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData: unknown): RangeFieldMeta; + pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData: unknown): CategoryFieldMeta; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 7b94e58f0e7d4..030d3a2a1ef87 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -17,7 +17,8 @@ import { scaleValue, getComputedFieldName } from '../style_util'; import React from 'react'; import { OrdinalLegend } from './components/ordinal_legend'; import { CategoricalLegend } from './components/categorical_legend'; -import { OrdinalFieldMetaOptionsPopover } from '../components/ordinal_field_meta_options_popover'; +import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; +import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover'; export class DynamicStyleProperty extends AbstractStyleProperty { static type = STYLE_TYPE.DYNAMIC; @@ -31,7 +32,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { getValueSuggestions = query => { const field = this.getField(); - const fieldSource = this.getFieldSource(); + const fieldSource = this._getFieldSource(); return fieldSource && field ? fieldSource.getValueSuggestions(field, query) : []; }; @@ -52,6 +53,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty { const fieldName = this.getFieldName(); const rangeFieldMetaFromLocalFeatures = styleMeta.getRangeFieldMetaDescriptor(fieldName); + if (!this.isFieldMetaEnabled()) { + return rangeFieldMetaFromLocalFeatures; + } + const dataRequestId = this._getStyleMetaDataRequestId(fieldName); if (!dataRequestId) { return rangeFieldMetaFromLocalFeatures; @@ -71,28 +76,32 @@ export class DynamicStyleProperty extends AbstractStyleProperty { const style = this._layer.getStyle(); const styleMeta = style.getStyleMeta(); const fieldName = this.getFieldName(); - const rangeFieldMetaFromLocalFeatures = styleMeta.getCategoryFieldMetaDescriptor(fieldName); + const categoryFieldMetaFromLocalFeatures = styleMeta.getCategoryFieldMetaDescriptor(fieldName); + + if (!this.isFieldMetaEnabled()) { + return categoryFieldMetaFromLocalFeatures; + } const dataRequestId = this._getStyleMetaDataRequestId(fieldName); if (!dataRequestId) { - return rangeFieldMetaFromLocalFeatures; + return categoryFieldMetaFromLocalFeatures; } const styleMetaDataRequest = this._layer.findDataRequestById(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { - return rangeFieldMetaFromLocalFeatures; + return categoryFieldMetaFromLocalFeatures; } const data = styleMetaDataRequest.getData(); const rangeFieldMeta = this.pluckCategoricalStyleMetaFromFieldMetaData(data); - return rangeFieldMeta ? rangeFieldMeta : rangeFieldMetaFromLocalFeatures; + return rangeFieldMeta ? rangeFieldMeta : categoryFieldMetaFromLocalFeatures; } getField() { return this._field; } - getFieldSource() { + _getFieldSource() { return this._field ? this._field.getSource() : null; } @@ -160,7 +169,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } } - supportsFeatureState() { + supportsMbFeatureState() { return true; } @@ -338,12 +347,14 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } renderFieldMetaPopover(onFieldMetaOptionsChange) { - if (!this.isOrdinal() || !this.supportsFieldMeta()) { + if (!this.supportsFieldMeta()) { return null; } - return ( - + return this.isCategorical() ? ( + + ) : ( + ); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js index 6a40a80a1a7a6..c561ec128dec5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js @@ -25,7 +25,7 @@ export class DynamicTextProperty extends DynamicStyleProperty { return false; } - supportsFeatureState() { + supportsMbFeatureState() { return false; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js index e08c2875c310e..7119b659c1232 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js @@ -6,7 +6,8 @@ import _ from 'lodash'; import { AbstractStyleProperty } from './style_property'; -import { DEFAULT_LABEL_SIZE, LABEL_BORDER_SIZES } from '../vector_style_defaults'; +import { DEFAULT_LABEL_SIZE } from '../vector_style_defaults'; +import { LABEL_BORDER_SIZES } from '../../../../../common/constants'; const SMALL_SIZE = 1 / 16; const MEDIUM_SIZE = 1 / 8; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js deleted file mode 100644 index c49fe46664025..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getVectorStyleLabel } from '../components/get_vector_style_label'; -export class AbstractStyleProperty { - constructor(options, styleName) { - this._options = options; - this._styleName = styleName; - } - - isDynamic() { - return false; - } - - /** - * Is the style fully defined and usable? (e.g. for rendering, in legend UX, ...) - * Why? during editing, partially-completed descriptors may be added to the layer-descriptor - * e.g. dynamic-fields can have an incomplete state when the field is not yet selected from the drop-down - * @returns {boolean} - */ - isComplete() { - return true; - } - - formatField(value) { - return value; - } - - getStyleName() { - return this._styleName; - } - - getOptions() { - return this._options || {}; - } - - renderRangeLegendHeader() { - return null; - } - - renderLegendDetailRow() { - return null; - } - - renderFieldMetaPopover() { - return null; - } - - getDisplayStyleName() { - return getVectorStyleLabel(this.getStyleName()); - } -} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts new file mode 100644 index 0000000000000..bba6cdb48e672 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { ReactElement } from 'react'; +// @ts-ignore +import { getVectorStyleLabel } from '../components/get_vector_style_label'; +import { + FieldMetaOptions, + StylePropertyOptions, +} from '../../../../../common/style_property_descriptor_types'; + +type LegendProps = { + isPointsOnly: boolean; + isLinesOnly: boolean; + symbolId?: string; +}; + +export interface IStyleProperty { + isDynamic(): boolean; + isComplete(): boolean; + formatField(value: string | undefined): string; + getStyleName(): string; + getOptions(): StylePropertyOptions; + renderRangeLegendHeader(): ReactElement | null; + renderLegendDetailRow(legendProps: LegendProps): ReactElement | null; + renderFieldMetaPopover( + onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void + ): ReactElement | null; + getDisplayStyleName(): string; +} + +export class AbstractStyleProperty implements IStyleProperty { + private _options: StylePropertyOptions; + private _styleName: string; + + constructor(options: StylePropertyOptions, styleName: string) { + this._options = options; + this._styleName = styleName; + } + + isDynamic(): boolean { + return false; + } + + /** + * Is the style fully defined and usable? (e.g. for rendering, in legend UX, ...) + * Why? during editing, partially-completed descriptors may be added to the layer-descriptor + * e.g. dynamic-fields can have an incomplete state when the field is not yet selected from the drop-down + * @returns {boolean} + */ + isComplete(): boolean { + return true; + } + + formatField(value: string | undefined): string { + // eslint-disable-next-line eqeqeq + return value == undefined ? '' : value; + } + + getStyleName(): string { + return this._styleName; + } + + getOptions(): StylePropertyOptions { + return this._options || {}; + } + + renderRangeLegendHeader() { + return null; + } + + renderLegendDetailRow() { + return null; + } + + renderFieldMetaPopover() { + return null; + } + + getDisplayStyleName() { + return getVectorStyleLabel(this.getStyleName()); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 528c5a9bfdc85..1c8ff3e205a38 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -508,7 +508,7 @@ export class VectorStyle extends AbstractStyle { const name = dynamicStyleProp.getField().getName(); const computedName = getComputedFieldName(dynamicStyleProp.getStyleName(), name); const styleValue = dynamicStyleProp.getMbValue(feature.properties[name]); - if (dynamicStyleProp.supportsFeatureState()) { + if (dynamicStyleProp.supportsMbFeatureState()) { tmpFeatureState[computedName] = styleValue; } else { feature.properties[computedName] = styleValue; @@ -523,7 +523,7 @@ export class VectorStyle extends AbstractStyle { //this return-value is used in an optimization for style-updates with mapbox-gl. //`true` indicates the entire data needs to reset on the source (otherwise the style-rules will not be reapplied) //`false` indicates the data does not need to be reset on the store, because styles are re-evaluated if they use featureState - return dynamicStyleProps.some(dynamicStyleProp => !dynamicStyleProp.supportsFeatureState()); + return dynamicStyleProps.some(dynamicStyleProp => !dynamicStyleProp.supportsMbFeatureState()); } arePointsSymbolizedAsCircles() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index 952f8766a6156..8bc397dd98b56 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -5,7 +5,7 @@ */ import { VectorStyle } from './vector_style'; -import { DEFAULT_ICON, SYMBOLIZE_AS_TYPES } from '../../../../common/constants'; +import { DEFAULT_ICON, LABEL_BORDER_SIZES, SYMBOLIZE_AS_TYPES } from '../../../../common/constants'; import { COLOR_GRADIENTS, COLOR_PALETTES, @@ -22,13 +22,6 @@ export const DEFAULT_SIGMA = 3; export const DEFAULT_LABEL_SIZE = 14; export const DEFAULT_ICON_SIZE = 6; -export const LABEL_BORDER_SIZES = { - NONE: 'NONE', - SMALL: 'SMALL', - MEDIUM: 'MEDIUM', - LARGE: 'LARGE', -}; - export const VECTOR_STYLES = { SYMBOLIZE_AS: 'symbolizeAs', FILL_COLOR: 'fillColor', @@ -147,6 +140,9 @@ export function getDefaultDynamicProperties() { options: { iconPaletteId: 'filledShapes', field: undefined, + fieldMetaOptions: { + isEnabled: true, + }, }, }, [VECTOR_STYLES.FILL_COLOR]: { diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts index e3ef744525d63..748b2fd1d782c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.d.ts @@ -8,12 +8,20 @@ import { AbstractLayer } from './layer'; import { IVectorSource } from './sources/vector_source'; import { VectorLayerDescriptor } from '../../common/descriptor_types'; +import { ILayer } from './layer'; +import { IJoin } from './joins/join'; type VectorLayerArguments = { source: IVectorSource; layerDescriptor: VectorLayerDescriptor; }; -export class VectorLayer extends AbstractLayer { +export interface IVectorLayer extends ILayer { + getValidJoins(): IJoin[]; +} + +export class VectorLayer extends AbstractLayer implements IVectorLayer { constructor(options: VectorLayerArguments); + + getValidJoins(): IJoin[]; } diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts index c3f90d815239c..e2d1d43295646 100644 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -10,6 +10,8 @@ import { wrapInI18nContext } from 'ui/i18n'; // @ts-ignore import { MapListing } from './components/map_listing'; // @ts-ignore +import { setInjectedVarFunc } from '../../../../plugins/maps/public/kibana_services'; // eslint-disable-line @kbn/eslint/no-restricted-paths +// @ts-ignore import { setLicenseId, setInspector, setFileUpload } from './kibana_services'; import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; @@ -33,9 +35,11 @@ interface MapsPluginSetupDependencies { export const bindSetupCoreAndPlugins = (core: CoreSetup, plugins: any) => { const { licensing } = plugins; + const { injectedMetadata } = core; if (licensing) { licensing.license$.subscribe(({ uid }: { uid: string }) => setLicenseId(uid)); } + setInjectedVarFunc(injectedMetadata.getInjectedVar); }; /** @internal */ @@ -53,7 +57,8 @@ export class MapsPlugin implements Plugin { } public start(core: CoreStart, plugins: any) { - setInspector(plugins.np.inspector); - setFileUpload(plugins.np.file_upload); + const { inspector, file_upload } = plugins.np; + setInspector(inspector); + setFileUpload(file_upload); } } diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index ab0926ab40070..e5eaf8870aa77 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -12,8 +12,13 @@ import { VectorLayer } from '../layers/vector_layer'; import { HeatmapLayer } from '../layers/heatmap_layer'; import { ALL_SOURCES } from '../layers/sources/all_sources'; import { timefilter } from 'ui/timefilter'; -import { getInspectorAdapters } from '../reducers/non_serializable_instances'; -import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; +import { + copyPersistentState, + TRACKED_LAYER_DESCRIPTOR, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/reducers/util'; import { InnerJoin } from '../layers/joins/inner_join'; function createLayerInstance(layerDescriptor, inspectorAdapters) { diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js index 995030d024ddf..5ec40a57ebc7f 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js @@ -9,7 +9,7 @@ jest.mock('../layers/heatmap_layer', () => {}); jest.mock('../layers/vector_tile_layer', () => {}); jest.mock('../layers/sources/all_sources', () => {}); jest.mock('../layers/joins/inner_join', () => {}); -jest.mock('../reducers/non_serializable_instances', () => ({ +jest.mock('../../../../../plugins/maps/public/reducers/non_serializable_instances', () => ({ getInspectorAdapters: () => { return {}; }, diff --git a/x-pack/legacy/plugins/ml/common/types/ml_server_info.ts b/x-pack/legacy/plugins/ml/common/types/ml_server_info.ts new file mode 100644 index 0000000000000..26dd1758827b4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/ml_server_info.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CategorizationAnalyzer } from './categories'; + +export interface MlServerDefaults { + anomaly_detectors: { + categorization_examples_limit?: number; + model_memory_limit?: string; + model_snapshot_retention_days?: number; + categorization_analyzer?: CategorizationAnalyzer; + }; + datafeeds: { scroll_size?: number }; +} + +export interface MlServerLimits { + max_model_memory_limit?: string; +} + +export interface MlInfoResponse { + defaults: MlServerDefaults; + limits: MlServerLimits; + native_code: { + build_hash: string; + version: string; + }; + upgrade_mode: boolean; + cloudId?: string; +} diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts index 87e19d09da30c..4e77687b56418 100644 --- a/x-pack/legacy/plugins/ml/common/types/modules.ts +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes } from 'src/core/public'; +import { SavedObjectAttributes } from 'kibana/public'; import { Datafeed, Job } from '../types/anomaly_detection_jobs'; export interface ModuleJob { diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 47df7c8c3e5e6..e138426e75724 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -10,7 +10,7 @@ import { Server } from 'src/legacy/server/kbn_server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // @ts-ignore: could not find declaration file for module import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -// @ts-ignore: could not find declaration file for module +// @ts-ignore: importing JSON file import mappings from './mappings'; export const ml = (kibana: any) => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx index 65fe36a7b611b..ceaf986b0d54f 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -77,7 +77,7 @@ class AnnotationFlyoutUI extends Component { const { annotation } = this.props; const toastNotifications = getToastNotifications(); - if (annotation === null) { + if (annotation === null || annotation._id === undefined) { return; } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index df2ca3e7de657..83afb489abcb0 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -19,11 +19,6 @@ import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; -interface GetDataFrameAnalyticsResponse { - count: number; - data_frame_analytics: DataFrameAnalyticsConfig[]; -} - export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -69,9 +64,7 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { const loadJobConfig = async () => { setIsLoadingJobConfig(true); try { - const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( - jobId - ); + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index ce72e90b4c230..fe96b056dea11 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -72,11 +72,6 @@ import { useMlContext } from '../../../../../contexts/ml'; const FEATURE_INFLUENCE = 'feature_influence'; -interface GetDataFrameAnalyticsResponse { - count: number; - data_frame_analytics: DataFrameAnalyticsConfig[]; -} - const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -130,9 +125,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { useEffect(() => { (async function() { - const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( - jobId - ); + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 569cf21792874..bb81682eeb5d7 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -19,11 +19,6 @@ import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; -interface GetDataFrameAnalyticsResponse { - count: number; - data_frame_analytics: DataFrameAnalyticsConfig[]; -} - export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -69,9 +64,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { const loadJobConfig = async () => { setIsLoadingJobConfig(true); try { - const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( - jobId - ); + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index c744c357c9550..983375ecd4f61 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -610,7 +610,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.excludedFieldsHelpText', { defaultMessage: - 'Optionally select fields to be excluded from analysis. All other supported fields will be included', + 'Select fields to exclude from analysis. All other supported fields are included.', })} error={ excludesOptions.length === 0 && diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx index 988daac528fd7..ffed1ebf522f4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx @@ -21,7 +21,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText', { defaultMessage: - 'Outlier detection jobs require a source index that is mapped as a table-like data structure and will only analyze numeric and boolean fields. Please use the advanced editor to add custom options to the configuration.', + 'Outlier detection jobs require a source index that is mapped as a table-like data structure and analyze only numeric and boolean fields. Use the advanced editor to add custom options to the configuration.', } ); @@ -29,7 +29,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText', { defaultMessage: - 'Regression jobs will only analyze numeric fields. Please use the advanced editor to apply custom options such as the prediction field name.', + 'Regression jobs analyze only numeric fields. Use the advanced editor to apply custom options, such as the prediction field name.', } ); @@ -37,7 +37,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.classificationHelpText', { defaultMessage: - 'Classification jobs require a source index that is mapped as a table-like data structure and supports fields that are numeric, boolean, text, keyword or ip. Please use the advanced editor to apply custom options such as the prediction field name.', + 'Classification jobs require a source index that is mapped as a table-like data structure and support fields that are numeric, boolean, text, keyword, or ip. Use the advanced editor to apply custom options, such as the prediction field name.', } ); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 350b3f98d4673..9a243e1b0316d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -8,7 +8,7 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; -import { SimpleSavedObject } from 'src/core/public'; +import { SimpleSavedObject } from 'kibana/public'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index 3c0c3fa0df87c..7383f565bd673 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -19,7 +19,7 @@ export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { const toastNotifications = getToastNotifications(); try { if (isDataFrameAnalyticsFailed(d.stats.state)) { - await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true, true); + await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true); } await ml.dataFrameAnalytics.deleteDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts index 1aaddda358082..7e337684371cd 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetDataFrameAnalyticsStatsResponseOk } from '../../../../../services/ml_api_service'; +import { GetDataFrameAnalyticsStatsResponseOk } from '../../../../../services/ml_api_service/data_frame_analytics'; import { getAnalyticsJobsStats } from './get_analytics'; import { DATA_FRAME_TASK_STATE } from '../../components/analytics_list/common'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index 1875216408c62..df58f225e62de 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -5,17 +5,12 @@ */ import { i18n } from '@kbn/i18n'; +import { ml } from '../../../../../services/ml_api_service'; import { - GetDataFrameAnalyticsStatsResponse, GetDataFrameAnalyticsStatsResponseError, GetDataFrameAnalyticsStatsResponseOk, - ml, -} from '../../../../../services/ml_api_service'; -import { - DataFrameAnalyticsConfig, - REFRESH_ANALYTICS_LIST_STATE, - refreshAnalyticsList$, -} from '../../../../common'; +} from '../../../../../services/ml_api_service/data_frame_analytics'; +import { REFRESH_ANALYTICS_LIST_STATE, refreshAnalyticsList$ } from '../../../../common'; import { DATA_FRAME_MODE, @@ -27,11 +22,6 @@ import { } from '../../components/analytics_list/common'; import { AnalyticStatsBarStats } from '../../../../../components/stats_bar'; -interface GetDataFrameAnalyticsResponse { - count: number; - data_frame_analytics: DataFrameAnalyticsConfig[]; -} - export const isGetDataFrameAnalyticsStatsResponseOk = ( arg: any ): arg is GetDataFrameAnalyticsStatsResponseOk => { @@ -125,8 +115,8 @@ export const getAnalyticsFactory = ( } try { - const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics(); - const analyticsStats: GetDataFrameAnalyticsStatsResponse = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(); + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(); const analyticsStatsResult = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) ? getAnalyticsJobsStats(analyticsStats) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts index c92c03c3b0f16..d277e3af1d5e6 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts @@ -20,8 +20,7 @@ export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => { try { await ml.dataFrameAnalytics.stopDataFrameAnalytics( d.config.id, - isDataFrameAnalyticsFailed(d.stats.state), - true + isDataFrameAnalyticsFailed(d.stats.state) ); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage', { diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 5c32d62c39f84..1bde182e9c61b 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -5,7 +5,7 @@ */ import React, { FC, Fragment } from 'react'; -import { IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient } from 'kibana/public'; import { useTimefilter } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js index 32b5634b143db..29e89022a5502 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js @@ -39,13 +39,12 @@ function randomNumber(min, max) { } function saveWatch(watchModel) { - const path = '/api/watcher'; - const url = `${path}/watch/${watchModel.id}`; + const path = `/api/watcher/watch/${watchModel.id}`; return http({ - url, + path, method: 'PUT', - data: watchModel.upstreamJSON, + body: JSON.stringify(watchModel.upstreamJSON), }); } @@ -187,10 +186,9 @@ class CreateWatchService { loadWatch(jobId) { const id = `ml-${jobId}`; - const path = '/api/watcher'; - const url = `${path}/watch/${id}`; + const path = `/api/watcher/watch/${id}`; return http({ - url, + path, method: 'GET', }); } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts index 85018ab2f7944..ea7ba21699f60 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts @@ -12,6 +12,7 @@ import { ml, } from '../../../../services/ml_api_service'; import { JobCreator } from '../job_creator'; +import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs'; export enum VALIDATOR_SEVERITY { ERROR, @@ -57,7 +58,7 @@ export function cardinalityValidator( return ml.validateCardinality$({ ...jobCreator.jobConfig, datafeed_config: jobCreator.datafeedConfig, - }); + } as CombinedJob); }), map(validationResults => { for (const validationResult of validationResults) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index ac7a2093d1f81..50c35ec426acb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -26,12 +26,10 @@ import { ml } from '../../../services/ml_api_service'; import { useMlContext } from '../../../contexts/ml'; import { DatafeedResponse, - DataRecognizerConfigResponse, JobOverride, JobResponse, KibanaObject, KibanaObjectResponse, - Module, ModuleJob, } from '../../../../../common/types/modules'; import { mlJobService } from '../../../services/job_service'; @@ -106,7 +104,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { */ const loadModule = async () => { try { - const response: Module = await ml.getDataRecognizerModule({ moduleId }); + const response = await ml.getDataRecognizerModule({ moduleId }); setJobs(response.jobs); const kibanaObjectsResult = await checkForSavedObjects(response.kibana as KibanaObjects); @@ -165,7 +163,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { let jobOverridesPayload: JobOverride[] | null = Object.values(jobOverrides); jobOverridesPayload = jobOverridesPayload.length > 0 ? jobOverridesPayload : null; - const response: DataRecognizerConfigResponse = await ml.setupDataRecognizerConfig({ + const response = await ml.setupDataRecognizerConfig({ moduleId, prefix: resultJobPrefix, query: tempQuery, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 835232a030383..96075e8940083 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient } from 'kibana/public'; import { esQuery, Query, esKuery } from '../../../../../../../../../src/plugins/data/public'; import { IIndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; diff --git a/x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts index d3bc498c50f82..b68f5d0be6645 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts @@ -5,12 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; import { JOBS_LIST_PATH } from './management_urls'; export function getJobsListBreadcrumbs() { return [ - MANAGEMENT_BREADCRUMB, { text: i18n.translate('xpack.ml.jobsList.breadcrumb', { defaultMessage: 'Jobs', diff --git a/x-pack/legacy/plugins/ml/public/application/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts index 99a2e8353a874..d3dd1e4227531 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/index.ts @@ -10,22 +10,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { npSetup } from 'ui/new_platform'; -import { management } from 'ui/management'; +import { npSetup, npStart } from 'ui/new_platform'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; import { metadata } from 'ui/metadata'; import { take } from 'rxjs/operators'; -import { JOBS_LIST_PATH } from './management_urls'; -import { setDependencyCache } from '../util/dependency_cache'; -import './jobs_list'; + +import { ManagementSetup } from '../../../../../../../src/plugins/management/public'; + import { LicensingPluginSetup, LICENSE_CHECK_STATE, } from '../../../../../../plugins/licensing/public'; + import { PLUGIN_ID } from '../../../common/constants/app'; import { MINIMUM_FULL_LICENSE } from '../../../common/license'; +import { setDependencyCache } from '../util/dependency_cache'; + +import { getJobsListBreadcrumbs } from './breadcrumbs'; +import { renderApp } from './jobs_list'; + type PluginsSetupExtended = typeof npSetup.plugins & { // adds licensing which isn't in the PluginsSetup interface, but does exist licensing: LicensingPluginSetup; @@ -36,11 +41,10 @@ const plugins = npSetup.plugins as PluginsSetupExtended; const licensing = plugins.licensing.license$.pipe(take(1)); licensing.subscribe(license => { if (license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === LICENSE_CHECK_STATE.Valid) { - initManagementSection(); + initManagementSection(plugins.management); } }); - -function initManagementSection() { +function initManagementSection(management: ManagementSetup) { const legacyBasePath = { prepend: chrome.addBasePath, get: chrome.getBasePath, @@ -54,22 +58,27 @@ function initManagementSection() { setDependencyCache({ docLinks: legacyDocLinks as any, basePath: legacyBasePath as any, + http: npStart.core.http, }); - management.register('ml', { - display: i18n.translate('xpack.ml.management.mlTitle', { + const mlSection = management.sections.register({ + id: 'ml', + title: i18n.translate('xpack.ml.management.mlTitle', { defaultMessage: 'Machine Learning', }), order: 100, icon: 'machineLearningApp', }); - management.getSection('ml').register('jobsList', { - name: 'jobsListLink', - order: 10, - display: i18n.translate('xpack.ml.management.jobsListTitle', { + mlSection.registerApp({ + id: 'jobsListLink', + title: i18n.translate('xpack.ml.management.jobsListTitle', { defaultMessage: 'Jobs list', }), - url: `#${JOBS_LIST_PATH}`, + order: 10, + async mount({ element, setBreadcrumbs }) { + setBreadcrumbs(getJobsListBreadcrumbs()); + return renderApp(element, {}); + }, }); } diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index a987ed7feeee9..f3080dcece989 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState } from 'react'; +import React, { useEffect, useState, Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; import { @@ -18,15 +18,14 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { getDocLinks } from '../../../../util/dependency_cache'; +import { checkGetManagementMlJobs } from '../../../../privilege/check_privilege'; + +import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; -interface Props { - isMlEnabledInSpace: boolean; -} interface Tab { id: string; name: string; @@ -65,11 +64,33 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] { ]; } -export const JobsListPage: FC = ({ isMlEnabledInSpace }) => { - const docLinks = getDocLinks(); - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; +export const JobsListPage: FC = () => { + const [initialized, setInitialized] = useState(false); + const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); const tabs = getTabs(isMlEnabledInSpace); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); + + const check = async () => { + try { + const checkPrivilege = await checkGetManagementMlJobs(); + setInitialized(true); + setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); + } catch (e) { + // Silent fail, `checkGetManagementMlJobs()` should redirect when + // there are insufficient permissions. + } + }; + + useEffect(() => { + check(); + }, []); + + if (initialized === false) { + return null; + } + + const docLinks = getDocLinks(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const anomalyDetectionJobsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-jobs.html`; const anomalyJobsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`; @@ -98,7 +119,7 @@ export const JobsListPage: FC = ({ isMlEnabledInSpace }) => { return ( - + diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts index b88138d139f60..1352fc1ef40b6 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts @@ -4,52 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import ReactDOM, { render, unmountComponentAtNode } from 'react-dom'; +import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import React from 'react'; -import routes from 'ui/routes'; -import { canGetManagementMlJobs } from '../../privilege/check_privilege'; -import { JOBS_LIST_PATH, ACCESS_DENIED_PATH } from '../management_urls'; -import { JobsListPage, AccessDeniedPage } from './components'; -import { getJobsListBreadcrumbs } from '../breadcrumbs'; +import { JobsListPage } from './components'; -const template = ` -
-`; +export const renderApp = (element: HTMLElement, appDependencies: any) => { + ReactDOM.render(React.createElement(JobsListPage), element); -routes.when(JOBS_LIST_PATH, { - template, - k7Breadcrumbs: getJobsListBreadcrumbs, - resolve: { - checkPrivilege: canGetManagementMlJobs, - }, - controller($scope, checkPrivilege) { - const { mlFeatureEnabledInSpace } = checkPrivilege; - - $scope.$on('$destroy', () => { - const elem = document.getElementById('kibanaManagementMLSection'); - if (elem) unmountComponentAtNode(elem); - }); - $scope.$$postDigest(() => { - const element = document.getElementById('kibanaManagementMLSection'); - ReactDOM.render( - React.createElement(JobsListPage, { isMlEnabledInSpace: mlFeatureEnabledInSpace }), - element - ); - }); - }, -}); - -routes.when(ACCESS_DENIED_PATH, { - template, - k7Breadcrumbs: getJobsListBreadcrumbs, - controller($scope) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('kibanaManagementMLSection'); - if (elem) unmountComponentAtNode(elem); - }); - $scope.$$postDigest(() => { - const element = document.getElementById('kibanaManagementMLSection'); - render(AccessDeniedPage(), element); - }); - }, -}); + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts index ec9695a2ce668..4de8c6eb703ff 100644 --- a/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts +++ b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts @@ -14,8 +14,8 @@ import { ACCESS_DENIED_PATH } from '../management/management_urls'; let privileges: Privileges = getDefaultPrivileges(); // manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53 -export function canGetManagementMlJobs() { - return new Promise((resolve, reject) => { +export function checkGetManagementMlJobs() { + return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => { getManageMlPrivileges().then( ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { privileges = capabilities; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts index 6d8138d4bcd2c..74dbe055fead3 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { ChromeBreadcrumb } from 'kibana/public'; export const ML_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx index 6b56bc154e801..23c3908c9af07 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { HashRouter, Route, RouteProps } from 'react-router-dom'; import { Location } from 'history'; -import { IUiSettingsClient, ChromeStart } from 'src/core/public'; +import { IUiSettingsClient, ChromeStart } from 'kibana/public'; import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { MlContext, MlContextValue } from '../contexts/ml'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts index ee4f77767fce8..6df7eee3d64a6 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts @@ -5,7 +5,7 @@ */ import { useEffect, useState } from 'react'; -import { IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient } from 'kibana/public'; import { getIndexPatternById, getIndexPatternsContract, diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts index 75db2470d77cc..0283041fa1148 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts @@ -5,7 +5,7 @@ */ import { Observable } from 'rxjs'; - +import { HttpFetchOptionsWithPath, HttpFetchOptions } from 'kibana/public'; import { getHttp } from '../util/dependency_cache'; function getResultHeaders(headers: HeadersInit): HeadersInit { @@ -16,64 +16,40 @@ function getResultHeaders(headers: HeadersInit): HeadersInit { } as HeadersInit; } -interface HttpOptions { - url: string; - method: string; - headers?: any; - data?: any; +function getFetchOptions( + options: HttpFetchOptionsWithPath +): { path: string; fetchOptions: HttpFetchOptions } { + if (!options.path) { + throw new Error('URL path is missing'); + } + return { + path: options.path, + fetchOptions: { + credentials: 'same-origin', + method: options.method || 'GET', + ...(options.body ? { body: options.body } : {}), + ...(options.query ? { query: options.query } : {}), + headers: getResultHeaders(options.headers ?? {}), + }, + }; } /** * Function for making HTTP requests to Kibana's backend. * Wrapper for Kibana's HttpHandler. */ -export async function http(options: HttpOptions) { - if (!options?.url) { - throw new Error('URL is missing'); - } - - try { - let url = ''; - url = url + (options.url || ''); - const headers = getResultHeaders(options.headers ?? {}); - - const allHeaders = options.headers === undefined ? headers : { ...options.headers, ...headers }; - const body = options.data === undefined ? null : JSON.stringify(options.data); - - const payload: RequestInit = { - method: options.method || 'GET', - headers: allHeaders, - credentials: 'same-origin', - }; - - if (body !== null) { - payload.body = body; - } - - return await getHttp().fetch(url, payload); - } catch (e) { - throw new Error(e); - } -} - -interface RequestOptions extends RequestInit { - body: BodyInit | any; +export async function http(options: HttpFetchOptionsWithPath): Promise { + const { path, fetchOptions } = getFetchOptions(options); + return getHttp().fetch(path, fetchOptions); } /** * Function for making HTTP requests to Kibana's backend which returns an Observable * with request cancellation support. */ -export function http$(url: string, options: RequestOptions): Observable { - const requestInit: RequestInit = { - ...options, - credentials: 'same-origin', - method: options.method || 'GET', - ...(options.body ? { body: JSON.stringify(options.body) as string } : {}), - headers: getResultHeaders(options.headers ?? {}), - }; - - return fromHttpHandler(url, requestInit); +export function http$(options: HttpFetchOptionsWithPath): Observable { + const { path, fetchOptions } = getFetchOptions(options); + return fromHttpHandler(path, fetchOptions); } /** diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts index cc30d481a6355..29a5732026761 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -15,21 +15,24 @@ export const annotations = { latestMs: number; maxAnnotations: number; }) { - return http$<{ annotations: Record }>(`${basePath()}/annotations`, { + const body = JSON.stringify(obj); + return http$<{ annotations: Record }>({ + path: `${basePath()}/annotations`, method: 'POST', - body: obj, + body, }); }, indexAnnotation(obj: any) { - return http({ - url: `${basePath()}/annotations/index`, + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/annotations/index`, method: 'PUT', - data: obj, + body, }); }, deleteAnnotation(id: string) { - return http({ - url: `${basePath()}/annotations/delete/${id}`, + return http({ + path: `${basePath()}/annotations/delete/${id}`, method: 'DELETE', }); }, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js deleted file mode 100644 index 8a74cddce3f6d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { http } from '../http_service'; - -import { basePath } from './index'; - -export const dataFrameAnalytics = { - getDataFrameAnalytics(analyticsId) { - const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; - return http({ - url: `${basePath()}/data_frame/analytics${analyticsIdString}`, - method: 'GET', - }); - }, - getDataFrameAnalyticsStats(analyticsId) { - if (analyticsId !== undefined) { - return http({ - url: `${basePath()}/data_frame/analytics/${analyticsId}/_stats`, - method: 'GET', - }); - } - - return http({ - url: `${basePath()}/data_frame/analytics/_stats`, - method: 'GET', - }); - }, - createDataFrameAnalytics(analyticsId, analyticsConfig) { - return http({ - url: `${basePath()}/data_frame/analytics/${analyticsId}`, - method: 'PUT', - data: analyticsConfig, - }); - }, - evaluateDataFrameAnalytics(evaluateConfig) { - return http({ - url: `${basePath()}/data_frame/_evaluate`, - method: 'POST', - data: evaluateConfig, - }); - }, - explainDataFrameAnalytics(jobConfig) { - return http({ - url: `${basePath()}/data_frame/analytics/_explain`, - method: 'POST', - data: jobConfig, - }); - }, - deleteDataFrameAnalytics(analyticsId) { - return http({ - url: `${basePath()}/data_frame/analytics/${analyticsId}`, - method: 'DELETE', - }); - }, - startDataFrameAnalytics(analyticsId) { - return http({ - url: `${basePath()}/data_frame/analytics/${analyticsId}/_start`, - method: 'POST', - }); - }, - stopDataFrameAnalytics(analyticsId, force = false) { - return http({ - url: `${basePath()}/data_frame/analytics/${analyticsId}/_stop?force=${force}`, - method: 'POST', - }); - }, - getAnalyticsAuditMessages(analyticsId) { - return http({ - url: `${basePath()}/data_frame/analytics/${analyticsId}/messages`, - method: 'GET', - }); - }, -}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts new file mode 100644 index 0000000000000..89950a659f609 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { http } from '../http_service'; + +import { basePath } from './index'; +import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common'; +import { DeepPartial } from '../../../../common/types/common'; + +export interface GetDataFrameAnalyticsStatsResponseOk { + node_failures?: object; + count: number; + data_frame_analytics: DataFrameAnalyticsStats[]; +} + +export interface GetDataFrameAnalyticsStatsResponseError { + statusCode: number; + error: string; + message: string; +} + +export type GetDataFrameAnalyticsStatsResponse = + | GetDataFrameAnalyticsStatsResponseOk + | GetDataFrameAnalyticsStatsResponseError; + +interface GetDataFrameAnalyticsResponse { + count: number; + data_frame_analytics: DataFrameAnalyticsConfig[]; +} + +export const dataFrameAnalytics = { + getDataFrameAnalytics(analyticsId?: string) { + const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; + return http({ + path: `${basePath()}/data_frame/analytics${analyticsIdString}`, + method: 'GET', + }); + }, + getDataFrameAnalyticsStats(analyticsId?: string) { + if (analyticsId !== undefined) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}/_stats`, + method: 'GET', + }); + } + + return http({ + path: `${basePath()}/data_frame/analytics/_stats`, + method: 'GET', + }); + }, + createDataFrameAnalytics( + analyticsId: string, + analyticsConfig: DeepPartial + ) { + const body = JSON.stringify(analyticsConfig); + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}`, + method: 'PUT', + body, + }); + }, + evaluateDataFrameAnalytics(evaluateConfig: any) { + const body = JSON.stringify(evaluateConfig); + return http({ + path: `${basePath()}/data_frame/_evaluate`, + method: 'POST', + body, + }); + }, + explainDataFrameAnalytics(jobConfig: DeepPartial) { + const body = JSON.stringify(jobConfig); + return http({ + path: `${basePath()}/data_frame/analytics/_explain`, + method: 'POST', + body, + }); + }, + deleteDataFrameAnalytics(analyticsId: string) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}`, + method: 'DELETE', + }); + }, + startDataFrameAnalytics(analyticsId: string) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}/_start`, + method: 'POST', + }); + }, + stopDataFrameAnalytics(analyticsId: string, force: boolean = false) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}/_stop`, + method: 'POST', + query: { force }, + }); + }, + getAnalyticsAuditMessages(analyticsId: string) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}/messages`, + method: 'GET', + }); + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js deleted file mode 100644 index 364fa57ba7d6b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { http } from '../http_service'; - -import { basePath } from './index'; - -export const fileDatavisualizer = { - analyzeFile(obj, params = {}) { - let paramString = ''; - if (Object.keys(params).length) { - paramString = '?'; - for (const p in params) { - if (params.hasOwnProperty(p)) { - paramString += `&${p}=${params[p]}`; - } - } - } - return http({ - url: `${basePath()}/file_data_visualizer/analyze_file${paramString}`, - method: 'POST', - data: obj, - }); - }, - - import(obj) { - const paramString = obj.id !== undefined ? `?id=${obj.id}` : ''; - const { index, data, settings, mappings, ingestPipeline } = obj; - - return http({ - url: `${basePath()}/file_data_visualizer/import${paramString}`, - method: 'POST', - data: { - index, - data, - settings, - mappings, - ingestPipeline, - }, - }); - }, -}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.ts new file mode 100644 index 0000000000000..9b492530d303d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { http } from '../http_service'; + +import { basePath } from './index'; + +export const fileDatavisualizer = { + analyzeFile(file: string, params: Record = {}) { + const body = JSON.stringify(file); + return http({ + path: `${basePath()}/file_data_visualizer/analyze_file`, + method: 'POST', + body, + query: params, + }); + }, + + import({ + id, + index, + data, + settings, + mappings, + ingestPipeline, + }: { + id: string; + index: string; + data: any; + settings: any; + mappings: any; + ingestPipeline: any; + }) { + const query = id !== undefined ? { id } : {}; + const body = JSON.stringify({ + index, + data, + settings, + mappings, + ingestPipeline, + }); + + return http({ + path: `${basePath()}/file_data_visualizer/import`, + method: 'POST', + query, + body, + }); + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js deleted file mode 100644 index 010a531a192f1..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// Service for querying filters, which hold lists of entities, -// for example a list of known safe URL domains. - -import { http } from '../http_service'; - -import { basePath } from './index'; - -export const filters = { - filters(obj) { - const filterId = obj && obj.filterId ? `/${obj.filterId}` : ''; - return http({ - url: `${basePath()}/filters${filterId}`, - method: 'GET', - }); - }, - - filtersStats() { - return http({ - url: `${basePath()}/filters/_stats`, - method: 'GET', - }); - }, - - addFilter(filterId, description, items) { - return http({ - url: `${basePath()}/filters`, - method: 'PUT', - data: { - filterId, - description, - items, - }, - }); - }, - - updateFilter(filterId, description, addItems, removeItems) { - const data = {}; - if (description !== undefined) { - data.description = description; - } - if (addItems !== undefined) { - data.addItems = addItems; - } - if (removeItems !== undefined) { - data.removeItems = removeItems; - } - - return http({ - url: `${basePath()}/filters/${filterId}`, - method: 'PUT', - data, - }); - }, - - deleteFilter(filterId) { - return http({ - url: `${basePath()}/filters/${filterId}`, - method: 'DELETE', - }); - }, -}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.ts new file mode 100644 index 0000000000000..f0c58966d0a8d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Service for querying filters, which hold lists of entities, +// for example a list of known safe URL domains. + +import { http } from '../http_service'; + +import { basePath } from './index'; + +export const filters = { + filters(obj?: { filterId?: string }) { + const filterId = obj && obj.filterId ? `/${obj.filterId}` : ''; + return http({ + path: `${basePath()}/filters${filterId}`, + method: 'GET', + }); + }, + + filtersStats() { + return http({ + path: `${basePath()}/filters/_stats`, + method: 'GET', + }); + }, + + addFilter(filterId: string, description: string, items: string[]) { + const body = JSON.stringify({ + filterId, + description, + items, + }); + return http({ + path: `${basePath()}/filters`, + method: 'PUT', + body, + }); + }, + + updateFilter(filterId: string, description: string, addItems: string[], removeItems: string[]) { + const body = JSON.stringify({ + ...(description !== undefined ? { description } : {}), + ...(addItems !== undefined ? { addItems } : {}), + ...(removeItems !== undefined ? { removeItems } : {}), + }); + + return http({ + path: `${basePath()}/filters/${filterId}`, + method: 'PUT', + body, + }); + }, + + deleteFilter(filterId: string) { + return http({ + path: `${basePath()}/filters/${filterId}`, + method: 'DELETE', + }); + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts deleted file mode 100644 index 97e001389c5f1..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Observable } from 'rxjs'; -import { Annotation } from '../../../../common/types/annotations'; -import { Dictionary } from '../../../../common/types/common'; -import { AggFieldNamePair } from '../../../../common/types/fields'; -import { Category } from '../../../../common/types/categories'; -import { ExistingJobsAndGroups } from '../job_service'; -import { PrivilegesResponse } from '../../../../common/types/privileges'; -import { MlServerDefaults, MlServerLimits } from '../ml_server_info'; -import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; -import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { JobMessage } from '../../../../common/types/audit_message'; -import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics'; -import { DeepPartial } from '../../../../common/types/common'; -import { PartitionFieldsDefinition } from '../results_service/result_service_rx'; -import { annotations } from './annotations'; -import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; -import { - MlJobWithTimeRange, - MlSummaryJobs, - CombinedJob, - JobId, -} from '../../../../common/types/anomaly_detection_jobs'; -import { - CategorizationAnalyzer, - CategoryFieldExample, - FieldExampleCheck, -} from '../../../../common/types/categories'; -import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/new_job'; - -declare const basePath: () => string; - -// TODO This is not a complete representation of all methods of `ml.*`. -// It just satisfies needs for other parts of the code area which use -// TypeScript and rely on the methods typed in here. -// This allows the import of `ml` into TypeScript code. -interface EsIndex { - name: string; -} - -export interface GetTimeFieldRangeResponse { - success: boolean; - start: { epoch: number; string: string }; - end: { epoch: number; string: string }; -} - -export interface BucketSpanEstimatorData { - aggTypes: Array; - duration: { - start: number; - end: number; - }; - fields: Array; - index: string; - query: any; - splitField: string | undefined; - timeField: string | undefined; -} - -export interface BucketSpanEstimatorResponse { - name: string; - ms: number; - error?: boolean; - message?: { msg: string } | string; -} - -export interface MlInfoResponse { - defaults: MlServerDefaults; - limits: MlServerLimits; - native_code: { - build_hash: string; - version: string; - }; - upgrade_mode: boolean; - cloudId?: string; -} - -export interface SuccessCardinality { - id: 'success_cardinality'; -} - -export interface CardinalityModelPlotHigh { - id: 'cardinality_model_plot_high'; - modelPlotCardinality: number; -} - -export type CardinalityValidationResult = SuccessCardinality | CardinalityModelPlotHigh; -export type CardinalityValidationResults = CardinalityValidationResult[]; - -declare interface Ml { - annotations: { - deleteAnnotation(id: string | undefined): Promise; - indexAnnotation(annotation: Annotation): Promise; - getAnnotations: typeof annotations.getAnnotations; - }; - - dataFrameAnalytics: { - getDataFrameAnalytics(analyticsId?: string): Promise; - getDataFrameAnalyticsStats(analyticsId?: string): Promise; - createDataFrameAnalytics(analyticsId: string, analyticsConfig: any): Promise; - evaluateDataFrameAnalytics(evaluateConfig: any): Promise; - explainDataFrameAnalytics(jobConfig: DeepPartial): Promise; - deleteDataFrameAnalytics(analyticsId: string): Promise; - startDataFrameAnalytics(analyticsId: string): Promise; - stopDataFrameAnalytics( - analyticsId: string, - force?: boolean, - waitForCompletion?: boolean - ): Promise; - getAnalyticsAuditMessages(analyticsId: string): Promise; - }; - - hasPrivileges(obj: object): Promise; - - checkMlPrivileges(): Promise; - checkManageMLPrivileges(): Promise; - getJobStats(obj: object): Promise; - getDatafeedStats(obj: object): Promise; - esSearch(obj: object): Promise; - esSearch$(obj: object): Observable; - getIndices(): Promise; - dataRecognizerModuleJobsExist(obj: { moduleId: string }): Promise; - getDataRecognizerModule(obj: { moduleId: string }): Promise; - setupDataRecognizerConfig(obj: object): Promise; - getTimeFieldRange(obj: object): Promise; - calculateModelMemoryLimit(obj: object): Promise<{ modelMemoryLimit: string }>; - calendars(obj?: { calendarId?: CalendarId; calendarIds?: CalendarId[] }): Promise; - updateCalendar(obj: UpdateCalendar): Promise; - - getVisualizerFieldStats(obj: object): Promise; - getVisualizerOverallStats(obj: object): Promise; - - results: { - getMaxAnomalyScore: (jobIds: string[], earliestMs: number, latestMs: number) => Promise; - fetchPartitionFieldsValues: ( - jobId: JobId, - searchTerm: Record, - criteriaFields: Array<{ fieldName: string; fieldValue: any }>, - earliestMs: number, - latestMs: number - ) => Observable; - }; - - jobs: { - jobsSummary(jobIds: string[]): Promise; - jobsWithTimerange( - dateFormatTz: string - ): Promise<{ jobs: MlJobWithTimeRange[]; jobsMap: Dictionary }>; - jobs(jobIds: string[]): Promise; - groups(): Promise; - updateGroups(updatedJobs: string[]): Promise; - forceStartDatafeeds(datafeedIds: string[], start: string, end: string): Promise; - stopDatafeeds(datafeedIds: string[]): Promise; - deleteJobs(jobIds: string[]): Promise; - closeJobs(jobIds: string[]): Promise; - jobAuditMessages(jobId: string, from?: string): Promise; - deletingJobTasks(): Promise; - newJobCaps(indexPatternTitle: string, isRollup: boolean): Promise; - newJobLineChart( - indexPatternTitle: string, - timeField: string, - start: number, - end: number, - intervalMs: number, - query: object, - aggFieldNamePairs: AggFieldNamePair[], - splitFieldName: string | null, - splitFieldValue: string | null - ): Promise; - newJobPopulationsChart( - indexPatternTitle: string, - timeField: string, - start: number, - end: number, - intervalMs: number, - query: object, - aggFieldNamePairs: AggFieldNamePair[], - splitFieldName: string - ): Promise; - getAllJobAndGroupIds(): Promise; - getLookBackProgress( - jobId: string, - start: number, - end: number - ): Promise<{ progress: number; isRunning: boolean; isJobClosed: boolean }>; - categorizationFieldExamples( - indexPatternTitle: string, - query: object, - size: number, - field: string, - timeField: string | undefined, - start: number, - end: number, - analyzer: CategorizationAnalyzer - ): Promise<{ - examples: CategoryFieldExample[]; - sampleSize: number; - overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS; - validationChecks: FieldExampleCheck[]; - }>; - topCategories( - jobId: string, - count: number - ): Promise<{ total: number; categories: Array<{ count?: number; category: Category }> }>; - }; - - estimateBucketSpan(data: BucketSpanEstimatorData): Promise; - - mlNodeCount(): Promise<{ count: number }>; - mlInfo(): Promise; - getCardinalityOfFields(obj: Record): any; - validateCardinality$(job: CombinedJob): Observable; -} - -declare const ml: Ml; - -export interface GetDataFrameAnalyticsStatsResponseOk { - node_failures?: object; - count: number; - data_frame_analytics: DataFrameAnalyticsStats[]; -} - -export interface GetDataFrameAnalyticsStatsResponseError { - statusCode: number; - error: string; - message: string; -} - -export type GetDataFrameAnalyticsStatsResponse = - | GetDataFrameAnalyticsStatsResponseOk - | GetDataFrameAnalyticsStatsResponseError; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js deleted file mode 100644 index 688abd1383ecb..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { pick } from 'lodash'; -import { http, http$ } from '../http_service'; - -import { annotations } from './annotations'; -import { dataFrameAnalytics } from './data_frame_analytics'; -import { filters } from './filters'; -import { results } from './results'; -import { jobs } from './jobs'; -import { fileDatavisualizer } from './datavisualizer'; - -export function basePath() { - return '/api/ml'; -} - -export const ml = { - getJobs(obj) { - const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; - return http({ - url: `${basePath()}/anomaly_detectors${jobId}`, - }); - }, - - getJobStats(obj) { - const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; - return http({ - url: `${basePath()}/anomaly_detectors${jobId}/_stats`, - }); - }, - - addJob(obj) { - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}`, - method: 'PUT', - data: obj.job, - }); - }, - - openJob(obj) { - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}/_open`, - method: 'POST', - }); - }, - - closeJob(obj) { - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}/_close`, - method: 'POST', - }); - }, - - deleteJob(obj) { - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}`, - method: 'DELETE', - }); - }, - - forceDeleteJob(obj) { - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}?force=true`, - method: 'DELETE', - }); - }, - - updateJob(obj) { - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}/_update`, - method: 'POST', - data: obj.job, - }); - }, - - estimateBucketSpan(obj) { - return http({ - url: `${basePath()}/validate/estimate_bucket_span`, - method: 'POST', - data: obj, - }); - }, - - validateJob(obj) { - return http({ - url: `${basePath()}/validate/job`, - method: 'POST', - data: obj, - }); - }, - - validateCardinality$(obj) { - return http$(`${basePath()}/validate/cardinality`, { - method: 'POST', - body: obj, - }); - }, - - getDatafeeds(obj) { - const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; - return http({ - url: `${basePath()}/datafeeds${datafeedId}`, - }); - }, - - getDatafeedStats(obj) { - const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; - return http({ - url: `${basePath()}/datafeeds${datafeedId}/_stats`, - }); - }, - - addDatafeed(obj) { - return http({ - url: `${basePath()}/datafeeds/${obj.datafeedId}`, - method: 'PUT', - data: obj.datafeedConfig, - }); - }, - - updateDatafeed(obj) { - return http({ - url: `${basePath()}/datafeeds/${obj.datafeedId}/_update`, - method: 'POST', - data: obj.datafeedConfig, - }); - }, - - deleteDatafeed(obj) { - return http({ - url: `${basePath()}/datafeeds/${obj.datafeedId}`, - method: 'DELETE', - }); - }, - - forceDeleteDatafeed(obj) { - return http({ - url: `${basePath()}/datafeeds/${obj.datafeedId}?force=true`, - method: 'DELETE', - }); - }, - - startDatafeed(obj) { - const data = {}; - if (obj.start !== undefined) { - data.start = obj.start; - } - if (obj.end !== undefined) { - data.end = obj.end; - } - return http({ - url: `${basePath()}/datafeeds/${obj.datafeedId}/_start`, - method: 'POST', - data, - }); - }, - - stopDatafeed(obj) { - return http({ - url: `${basePath()}/datafeeds/${obj.datafeedId}/_stop`, - method: 'POST', - }); - }, - - datafeedPreview(obj) { - return http({ - url: `${basePath()}/datafeeds/${obj.datafeedId}/_preview`, - method: 'GET', - }); - }, - - validateDetector(obj) { - return http({ - url: `${basePath()}/anomaly_detectors/_validate/detector`, - method: 'POST', - data: obj.detector, - }); - }, - - forecast(obj) { - const data = {}; - if (obj.duration !== undefined) { - data.duration = obj.duration; - } - - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}/_forecast`, - method: 'POST', - data, - }); - }, - - overallBuckets(obj) { - const data = pick(obj, ['topN', 'bucketSpan', 'start', 'end']); - return http({ - url: `${basePath()}/anomaly_detectors/${obj.jobId}/results/overall_buckets`, - method: 'POST', - data, - }); - }, - - hasPrivileges(obj) { - return http({ - url: `${basePath()}/_has_privileges`, - method: 'POST', - data: obj, - }); - }, - - checkMlPrivileges() { - return http({ - url: `${basePath()}/ml_capabilities`, - method: 'GET', - }); - }, - - checkManageMLPrivileges() { - return http({ - url: `${basePath()}/ml_capabilities?ignoreSpaces=true`, - method: 'GET', - }); - }, - - getNotificationSettings() { - return http({ - url: `${basePath()}/notification_settings`, - method: 'GET', - }); - }, - - getFieldCaps(obj) { - const data = {}; - if (obj.index !== undefined) { - data.index = obj.index; - } - if (obj.fields !== undefined) { - data.fields = obj.fields; - } - return http({ - url: `${basePath()}/indices/field_caps`, - method: 'POST', - data, - }); - }, - - recognizeIndex(obj) { - return http({ - url: `${basePath()}/modules/recognize/${obj.indexPatternTitle}`, - method: 'GET', - }); - }, - - listDataRecognizerModules() { - return http({ - url: `${basePath()}/modules/get_module`, - method: 'GET', - }); - }, - - getDataRecognizerModule(obj) { - return http({ - url: `${basePath()}/modules/get_module/${obj.moduleId}`, - method: 'GET', - }); - }, - - dataRecognizerModuleJobsExist(obj) { - return http({ - url: `${basePath()}/modules/jobs_exist/${obj.moduleId}`, - method: 'GET', - }); - }, - - setupDataRecognizerConfig(obj) { - const data = pick(obj, [ - 'prefix', - 'groups', - 'indexPatternName', - 'query', - 'useDedicatedIndex', - 'startDatafeed', - 'start', - 'end', - 'jobOverrides', - ]); - - return http({ - url: `${basePath()}/modules/setup/${obj.moduleId}`, - method: 'POST', - data, - }); - }, - - getVisualizerFieldStats(obj) { - const data = pick(obj, [ - 'query', - 'timeFieldName', - 'earliest', - 'latest', - 'samplerShardSize', - 'interval', - 'fields', - 'maxExamples', - ]); - - return http({ - url: `${basePath()}/data_visualizer/get_field_stats/${obj.indexPatternTitle}`, - method: 'POST', - data, - }); - }, - - getVisualizerOverallStats(obj) { - const data = pick(obj, [ - 'query', - 'timeFieldName', - 'earliest', - 'latest', - 'samplerShardSize', - 'aggregatableFields', - 'nonAggregatableFields', - ]); - - return http({ - url: `${basePath()}/data_visualizer/get_overall_stats/${obj.indexPatternTitle}`, - method: 'POST', - data, - }); - }, - - /** - * Gets a list of calendars - * @param obj - * @returns {Promise} - */ - calendars(obj = {}) { - const { calendarId, calendarIds } = obj; - let calendarIdsPathComponent = ''; - if (calendarId) { - calendarIdsPathComponent = `/${calendarId}`; - } else if (calendarIds) { - calendarIdsPathComponent = `/${calendarIds.join(',')}`; - } - return http({ - url: `${basePath()}/calendars${calendarIdsPathComponent}`, - method: 'GET', - }); - }, - - addCalendar(obj) { - return http({ - url: `${basePath()}/calendars`, - method: 'PUT', - data: obj, - }); - }, - - updateCalendar(obj) { - const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : ''; - return http({ - url: `${basePath()}/calendars${calendarId}`, - method: 'PUT', - data: obj, - }); - }, - - deleteCalendar(obj) { - return http({ - url: `${basePath()}/calendars/${obj.calendarId}`, - method: 'DELETE', - }); - }, - - mlNodeCount() { - return http({ - url: `${basePath()}/ml_node_count`, - method: 'GET', - }); - }, - - mlInfo() { - return http({ - url: `${basePath()}/info`, - method: 'GET', - }); - }, - - calculateModelMemoryLimit(obj) { - const data = pick(obj, [ - 'indexPattern', - 'splitFieldName', - 'query', - 'fieldNames', - 'influencerNames', - 'timeFieldName', - 'earliestMs', - 'latestMs', - ]); - - return http({ - url: `${basePath()}/validate/calculate_model_memory_limit`, - method: 'POST', - data, - }); - }, - - getCardinalityOfFields(obj) { - const data = pick(obj, [ - 'index', - 'fieldNames', - 'query', - 'timeFieldName', - 'earliestMs', - 'latestMs', - ]); - - return http({ - url: `${basePath()}/fields_service/field_cardinality`, - method: 'POST', - data, - }); - }, - - getTimeFieldRange(obj) { - const data = pick(obj, ['index', 'timeFieldName', 'query']); - - return http({ - url: `${basePath()}/fields_service/time_field_range`, - method: 'POST', - data, - }); - }, - - esSearch(obj) { - return http({ - url: `${basePath()}/es_search`, - method: 'POST', - data: obj, - }); - }, - - esSearch$(obj) { - return http$(`${basePath()}/es_search`, { - method: 'POST', - body: obj, - }); - }, - - getIndices() { - const tempBasePath = '/api'; - return http({ - url: `${tempBasePath}/index_management/indices`, - method: 'GET', - }); - }, - - annotations, - dataFrameAnalytics, - filters, - results, - jobs, - fileDatavisualizer, -}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.ts new file mode 100644 index 0000000000000..b8e21898a4bb3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.ts @@ -0,0 +1,645 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { http, http$ } from '../http_service'; + +import { annotations } from './annotations'; +import { dataFrameAnalytics } from './data_frame_analytics'; +import { filters } from './filters'; +import { results } from './results'; +import { jobs } from './jobs'; +import { fileDatavisualizer } from './datavisualizer'; +import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info'; + +import { PrivilegesResponse } from '../../../../common/types/privileges'; +import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; +import { + Job, + Datafeed, + CombinedJob, + Detector, +} from '../../../../common/types/anomaly_detection_jobs'; +import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { FieldRequestConfig } from '../../datavisualizer/index_based/common'; +import { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules'; + +export interface MlInfoResponse { + defaults: MlServerDefaults; + limits: MlServerLimits; + native_code: { + build_hash: string; + version: string; + }; + upgrade_mode: boolean; + cloudId?: string; +} + +export interface BucketSpanEstimatorData { + aggTypes: Array; + duration: { + start: number; + end: number; + }; + fields: Array; + index: string; + query: any; + splitField: string | undefined; + timeField: string | undefined; +} + +export interface BucketSpanEstimatorResponse { + name: string; + ms: number; + error?: boolean; + message?: { msg: string } | string; +} + +export interface GetTimeFieldRangeResponse { + success: boolean; + start: { epoch: number; string: string }; + end: { epoch: number; string: string }; +} + +export interface SuccessCardinality { + id: 'success_cardinality'; +} + +export interface CardinalityModelPlotHigh { + id: 'cardinality_model_plot_high'; + modelPlotCardinality: number; +} + +export type CardinalityValidationResult = SuccessCardinality | CardinalityModelPlotHigh; +export type CardinalityValidationResults = CardinalityValidationResult[]; + +export function basePath() { + return '/api/ml'; +} + +export const ml = { + getJobs(obj?: { jobId?: string }) { + const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; + return http({ + path: `${basePath()}/anomaly_detectors${jobId}`, + }); + }, + + getJobStats(obj: { jobId?: string }) { + const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; + return http({ + path: `${basePath()}/anomaly_detectors${jobId}/_stats`, + }); + }, + + addJob({ jobId, job }: { jobId: string; job: Job }) { + const body = JSON.stringify(job); + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}`, + method: 'PUT', + body, + }); + }, + + openJob({ jobId }: { jobId: string }) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/_open`, + method: 'POST', + }); + }, + + closeJob({ jobId }: { jobId: string }) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/_close`, + method: 'POST', + }); + }, + + deleteJob({ jobId }: { jobId: string }) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}`, + method: 'DELETE', + }); + }, + + forceDeleteJob({ jobId }: { jobId: string }) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}?force=true`, + method: 'DELETE', + }); + }, + + updateJob({ jobId, job }: { jobId: string; job: Job }) { + const body = JSON.stringify(job); + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/_update`, + method: 'POST', + body, + }); + }, + + estimateBucketSpan(obj: BucketSpanEstimatorData) { + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/validate/estimate_bucket_span`, + method: 'POST', + body, + }); + }, + + validateJob({ job }: { job: Job }) { + const body = JSON.stringify({ job }); + return http({ + path: `${basePath()}/validate/job`, + method: 'POST', + body, + }); + }, + + validateCardinality$(job: CombinedJob): Observable { + const body = JSON.stringify(job); + return http$({ + path: `${basePath()}/validate/cardinality`, + method: 'POST', + body, + }); + }, + + getDatafeeds(obj: { datafeedId: string }) { + const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; + return http({ + path: `${basePath()}/datafeeds${datafeedId}`, + }); + }, + + getDatafeedStats(obj: { datafeedId: string }) { + const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; + return http({ + path: `${basePath()}/datafeeds${datafeedId}/_stats`, + }); + }, + + addDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) { + const body = JSON.stringify(datafeedConfig); + return http({ + path: `${basePath()}/datafeeds/${datafeedId}`, + method: 'PUT', + body, + }); + }, + + updateDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) { + const body = JSON.stringify(datafeedConfig); + return http({ + path: `${basePath()}/datafeeds/${datafeedId}/_update`, + method: 'POST', + body, + }); + }, + + deleteDatafeed({ datafeedId }: { datafeedId: string }) { + return http({ + path: `${basePath()}/datafeeds/${datafeedId}`, + method: 'DELETE', + }); + }, + + forceDeleteDatafeed({ datafeedId }: { datafeedId: string }) { + return http({ + path: `${basePath()}/datafeeds/${datafeedId}?force=true`, + method: 'DELETE', + }); + }, + + startDatafeed({ datafeedId, start, end }: { datafeedId: string; start: number; end: number }) { + const body = JSON.stringify({ + ...(start !== undefined ? { start } : {}), + ...(end !== undefined ? { end } : {}), + }); + + return http({ + path: `${basePath()}/datafeeds/${datafeedId}/_start`, + method: 'POST', + body, + }); + }, + + stopDatafeed({ datafeedId }: { datafeedId: string }) { + return http({ + path: `${basePath()}/datafeeds/${datafeedId}/_stop`, + method: 'POST', + }); + }, + + datafeedPreview({ datafeedId }: { datafeedId: string }) { + return http({ + path: `${basePath()}/datafeeds/${datafeedId}/_preview`, + method: 'GET', + }); + }, + + validateDetector({ detector }: { detector: Detector }) { + const body = JSON.stringify(detector); + return http({ + path: `${basePath()}/anomaly_detectors/_validate/detector`, + method: 'POST', + body, + }); + }, + + forecast({ jobId, duration }: { jobId: string; duration?: string }) { + const body = JSON.stringify({ + ...(duration !== undefined ? { duration } : {}), + }); + + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/_forecast`, + method: 'POST', + body, + }); + }, + + overallBuckets({ + jobId, + topN, + bucketSpan, + start, + end, + }: { + jobId: string; + topN: string; + bucketSpan: string; + start: number; + end: number; + }) { + const body = JSON.stringify({ topN, bucketSpan, start, end }); + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/results/overall_buckets`, + method: 'POST', + body, + }); + }, + + hasPrivileges(obj: any) { + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/_has_privileges`, + method: 'POST', + body, + }); + }, + + checkMlPrivileges() { + return http({ + path: `${basePath()}/ml_capabilities`, + method: 'GET', + }); + }, + + checkManageMLPrivileges() { + return http({ + path: `${basePath()}/ml_capabilities`, + method: 'GET', + query: { ignoreSpaces: true }, + }); + }, + + getNotificationSettings() { + return http({ + path: `${basePath()}/notification_settings`, + method: 'GET', + }); + }, + + getFieldCaps({ index, fields }: { index: string; fields: string[] }) { + const body = JSON.stringify({ + ...(index !== undefined ? { index } : {}), + ...(fields !== undefined ? { fields } : {}), + }); + + return http({ + path: `${basePath()}/indices/field_caps`, + method: 'POST', + body, + }); + }, + + recognizeIndex({ indexPatternTitle }: { indexPatternTitle: string }) { + return http({ + path: `${basePath()}/modules/recognize/${indexPatternTitle}`, + method: 'GET', + }); + }, + + listDataRecognizerModules() { + return http({ + path: `${basePath()}/modules/get_module`, + method: 'GET', + }); + }, + + getDataRecognizerModule({ moduleId }: { moduleId: string }) { + return http({ + path: `${basePath()}/modules/get_module/${moduleId}`, + method: 'GET', + }); + }, + + dataRecognizerModuleJobsExist({ moduleId }: { moduleId: string }) { + return http({ + path: `${basePath()}/modules/jobs_exist/${moduleId}`, + method: 'GET', + }); + }, + + setupDataRecognizerConfig({ + moduleId, + prefix, + groups, + indexPatternName, + query, + useDedicatedIndex, + startDatafeed, + start, + end, + jobOverrides, + }: { + moduleId: string; + prefix?: string; + groups?: string[]; + indexPatternName?: string; + query?: any; + useDedicatedIndex?: boolean; + startDatafeed?: boolean; + start?: number; + end?: number; + jobOverrides?: Array>; + }) { + const body = JSON.stringify({ + prefix, + groups, + indexPatternName, + query, + useDedicatedIndex, + startDatafeed, + start, + end, + jobOverrides, + }); + + return http({ + path: `${basePath()}/modules/setup/${moduleId}`, + method: 'POST', + body, + }); + }, + + getVisualizerFieldStats({ + indexPatternTitle, + query, + timeFieldName, + earliest, + latest, + samplerShardSize, + interval, + fields, + maxExamples, + }: { + indexPatternTitle: string; + query: any; + timeFieldName?: string; + earliest?: number; + latest?: number; + samplerShardSize?: number; + interval?: string; + fields?: FieldRequestConfig[]; + maxExamples?: number; + }) { + const body = JSON.stringify({ + query, + timeFieldName, + earliest, + latest, + samplerShardSize, + interval, + fields, + maxExamples, + }); + + return http({ + path: `${basePath()}/data_visualizer/get_field_stats/${indexPatternTitle}`, + method: 'POST', + body, + }); + }, + + getVisualizerOverallStats({ + indexPatternTitle, + query, + timeFieldName, + earliest, + latest, + samplerShardSize, + aggregatableFields, + nonAggregatableFields, + }: { + indexPatternTitle: string; + query: any; + timeFieldName?: string; + earliest?: number; + latest?: number; + samplerShardSize?: number; + aggregatableFields: string[]; + nonAggregatableFields: string[]; + }) { + const body = JSON.stringify({ + query, + timeFieldName, + earliest, + latest, + samplerShardSize, + aggregatableFields, + nonAggregatableFields, + }); + + return http({ + path: `${basePath()}/data_visualizer/get_overall_stats/${indexPatternTitle}`, + method: 'POST', + body, + }); + }, + + /** + * Gets a list of calendars + * @param obj + * @returns {Promise} + */ + calendars(obj?: { calendarId?: CalendarId; calendarIds?: CalendarId[] }) { + const { calendarId, calendarIds } = obj || {}; + let calendarIdsPathComponent = ''; + if (calendarId) { + calendarIdsPathComponent = `/${calendarId}`; + } else if (calendarIds) { + calendarIdsPathComponent = `/${calendarIds.join(',')}`; + } + return http({ + path: `${basePath()}/calendars${calendarIdsPathComponent}`, + method: 'GET', + }); + }, + + addCalendar(obj: Calendar) { + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/calendars`, + method: 'PUT', + body, + }); + }, + + updateCalendar(obj: UpdateCalendar) { + const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : ''; + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/calendars${calendarId}`, + method: 'PUT', + body, + }); + }, + + deleteCalendar({ calendarId }: { calendarId?: string }) { + return http({ + path: `${basePath()}/calendars/${calendarId}`, + method: 'DELETE', + }); + }, + + mlNodeCount() { + return http<{ count: number }>({ + path: `${basePath()}/ml_node_count`, + method: 'GET', + }); + }, + + mlInfo() { + return http({ + path: `${basePath()}/info`, + method: 'GET', + }); + }, + + calculateModelMemoryLimit({ + indexPattern, + splitFieldName, + query, + fieldNames, + influencerNames, + timeFieldName, + earliestMs, + latestMs, + }: { + indexPattern: string; + splitFieldName: string; + query: any; + fieldNames: string[]; + influencerNames: string[]; + timeFieldName: string; + earliestMs: number; + latestMs: number; + }) { + const body = JSON.stringify({ + indexPattern, + splitFieldName, + query, + fieldNames, + influencerNames, + timeFieldName, + earliestMs, + latestMs, + }); + + return http<{ modelMemoryLimit: string }>({ + path: `${basePath()}/validate/calculate_model_memory_limit`, + method: 'POST', + body, + }); + }, + + getCardinalityOfFields({ + index, + fieldNames, + query, + timeFieldName, + earliestMs, + latestMs, + }: { + index: string; + fieldNames: string[]; + query: any; + timeFieldName: string; + earliestMs: number; + latestMs: number; + }) { + const body = JSON.stringify({ index, fieldNames, query, timeFieldName, earliestMs, latestMs }); + + return http({ + path: `${basePath()}/fields_service/field_cardinality`, + method: 'POST', + body, + }); + }, + + getTimeFieldRange({ + index, + timeFieldName, + query, + }: { + index: string; + timeFieldName?: string; + query: any; + }) { + const body = JSON.stringify({ index, timeFieldName, query }); + + return http({ + path: `${basePath()}/fields_service/time_field_range`, + method: 'POST', + body, + }); + }, + + esSearch(obj: any) { + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/es_search`, + method: 'POST', + body, + }); + }, + + esSearch$(obj: any) { + const body = JSON.stringify(obj); + return http$({ + path: `${basePath()}/es_search`, + method: 'POST', + body, + }); + }, + + getIndices() { + const tempBasePath = '/api'; + return http>({ + path: `${tempBasePath}/index_management/indices`, + method: 'GET', + }); + }, + + annotations, + dataFrameAnalytics, + filters, + results, + jobs, + fileDatavisualizer, +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js deleted file mode 100644 index 1ac391c7f84ae..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { http } from '../http_service'; - -import { basePath } from './index'; - -export const jobs = { - jobsSummary(jobIds) { - return http({ - url: `${basePath()}/jobs/jobs_summary`, - method: 'POST', - data: { - jobIds, - }, - }); - }, - - jobsWithTimerange(dateFormatTz) { - return http({ - url: `${basePath()}/jobs/jobs_with_time_range`, - method: 'POST', - data: { - dateFormatTz, - }, - }); - }, - - jobs(jobIds) { - return http({ - url: `${basePath()}/jobs/jobs`, - method: 'POST', - data: { - jobIds, - }, - }); - }, - - groups() { - return http({ - url: `${basePath()}/jobs/groups`, - method: 'GET', - }); - }, - - updateGroups(updatedJobs) { - return http({ - url: `${basePath()}/jobs/update_groups`, - method: 'POST', - data: { - jobs: updatedJobs, - }, - }); - }, - - forceStartDatafeeds(datafeedIds, start, end) { - return http({ - url: `${basePath()}/jobs/force_start_datafeeds`, - method: 'POST', - data: { - datafeedIds, - start, - end, - }, - }); - }, - - stopDatafeeds(datafeedIds) { - return http({ - url: `${basePath()}/jobs/stop_datafeeds`, - method: 'POST', - data: { - datafeedIds, - }, - }); - }, - - deleteJobs(jobIds) { - return http({ - url: `${basePath()}/jobs/delete_jobs`, - method: 'POST', - data: { - jobIds, - }, - }); - }, - - closeJobs(jobIds) { - return http({ - url: `${basePath()}/jobs/close_jobs`, - method: 'POST', - data: { - jobIds, - }, - }); - }, - - jobAuditMessages(jobId, from) { - const jobIdString = jobId !== undefined ? `/${jobId}` : ''; - const fromString = from !== undefined ? `?from=${from}` : ''; - return http({ - url: `${basePath()}/job_audit_messages/messages${jobIdString}${fromString}`, - method: 'GET', - }); - }, - - deletingJobTasks() { - return http({ - url: `${basePath()}/jobs/deleting_jobs_tasks`, - method: 'GET', - }); - }, - - jobsExist(jobIds) { - return http({ - url: `${basePath()}/jobs/jobs_exist`, - method: 'POST', - data: { - jobIds, - }, - }); - }, - - newJobCaps(indexPatternTitle, isRollup = false) { - const isRollupString = isRollup === true ? `?rollup=true` : ''; - return http({ - url: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}${isRollupString}`, - method: 'GET', - }); - }, - - newJobLineChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue - ) { - return http({ - url: `${basePath()}/jobs/new_job_line_chart`, - method: 'POST', - data: { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue, - }, - }); - }, - - newJobPopulationsChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName - ) { - return http({ - url: `${basePath()}/jobs/new_job_population_chart`, - method: 'POST', - data: { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - }, - }); - }, - - getAllJobAndGroupIds() { - return http({ - url: `${basePath()}/jobs/all_jobs_and_group_ids`, - method: 'GET', - }); - }, - - getLookBackProgress(jobId, start, end) { - return http({ - url: `${basePath()}/jobs/look_back_progress`, - method: 'POST', - data: { - jobId, - start, - end, - }, - }); - }, - - categorizationFieldExamples( - indexPatternTitle, - query, - size, - field, - timeField, - start, - end, - analyzer - ) { - return http({ - url: `${basePath()}/jobs/categorization_field_examples`, - method: 'POST', - data: { - indexPatternTitle, - query, - size, - field, - timeField, - start, - end, - analyzer, - }, - }); - }, - - topCategories(jobId, count) { - return http({ - url: `${basePath()}/jobs/top_categories`, - method: 'POST', - data: { - jobId, - count, - }, - }); - }, -}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.ts new file mode 100644 index 0000000000000..bcceffb14123e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { http } from '../http_service'; + +import { basePath } from './index'; +import { Dictionary } from '../../../../common/types/common'; +import { MlJobWithTimeRange, MlSummaryJobs } from '../../../../common/types/anomaly_detection_jobs'; +import { JobMessage } from '../../../../common/types/audit_message'; +import { AggFieldNamePair } from '../../../../common/types/fields'; +import { ExistingJobsAndGroups } from '../job_service'; +import { + CategorizationAnalyzer, + CategoryFieldExample, + FieldExampleCheck, +} from '../../../../common/types/categories'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/new_job'; +import { Category } from '../../../../common/types/categories'; + +export const jobs = { + jobsSummary(jobIds: string[]) { + const body = JSON.stringify({ jobIds }); + return http({ + path: `${basePath()}/jobs/jobs_summary`, + method: 'POST', + body, + }); + }, + + jobsWithTimerange(dateFormatTz: string) { + const body = JSON.stringify({ dateFormatTz }); + return http<{ jobs: MlJobWithTimeRange[]; jobsMap: Dictionary }>({ + path: `${basePath()}/jobs/jobs_with_time_range`, + method: 'POST', + body, + }); + }, + + jobs(jobIds: string[]) { + const body = JSON.stringify({ jobIds }); + return http({ + path: `${basePath()}/jobs/jobs`, + method: 'POST', + body, + }); + }, + + groups() { + return http({ + path: `${basePath()}/jobs/groups`, + method: 'GET', + }); + }, + + updateGroups(updatedJobs: string[]) { + const body = JSON.stringify({ jobs: updatedJobs }); + return http({ + path: `${basePath()}/jobs/update_groups`, + method: 'POST', + body, + }); + }, + + forceStartDatafeeds(datafeedIds: string[], start: string, end: string) { + const body = JSON.stringify({ + datafeedIds, + start, + end, + }); + + return http({ + path: `${basePath()}/jobs/force_start_datafeeds`, + method: 'POST', + body, + }); + }, + + stopDatafeeds(datafeedIds: string[]) { + const body = JSON.stringify({ datafeedIds }); + return http({ + path: `${basePath()}/jobs/stop_datafeeds`, + method: 'POST', + body, + }); + }, + + deleteJobs(jobIds: string[]) { + const body = JSON.stringify({ jobIds }); + return http({ + path: `${basePath()}/jobs/delete_jobs`, + method: 'POST', + body, + }); + }, + + closeJobs(jobIds: string[]) { + const body = JSON.stringify({ jobIds }); + return http({ + path: `${basePath()}/jobs/close_jobs`, + method: 'POST', + body, + }); + }, + + jobAuditMessages(jobId: string, from?: number) { + const jobIdString = jobId !== undefined ? `/${jobId}` : ''; + const query = from !== undefined ? { from } : {}; + return http({ + path: `${basePath()}/job_audit_messages/messages${jobIdString}`, + method: 'GET', + query, + }); + }, + + deletingJobTasks() { + return http({ + path: `${basePath()}/jobs/deleting_jobs_tasks`, + method: 'GET', + }); + }, + + jobsExist(jobIds: string[]) { + const body = JSON.stringify({ jobIds }); + return http({ + path: `${basePath()}/jobs/jobs_exist`, + method: 'POST', + body, + }); + }, + + newJobCaps(indexPatternTitle: string, isRollup: boolean = false) { + const query = isRollup === true ? { rollup: true } : {}; + return http({ + path: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}`, + method: 'GET', + query, + }); + }, + + newJobLineChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: any, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string | null, + splitFieldValue: string | null + ) { + const body = JSON.stringify({ + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue, + }); + return http({ + path: `${basePath()}/jobs/new_job_line_chart`, + method: 'POST', + body, + }); + }, + + newJobPopulationsChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: any, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string + ) { + const body = JSON.stringify({ + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + }); + return http({ + path: `${basePath()}/jobs/new_job_population_chart`, + method: 'POST', + body, + }); + }, + + getAllJobAndGroupIds() { + return http({ + path: `${basePath()}/jobs/all_jobs_and_group_ids`, + method: 'GET', + }); + }, + + getLookBackProgress(jobId: string, start: number, end: number) { + const body = JSON.stringify({ + jobId, + start, + end, + }); + return http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({ + path: `${basePath()}/jobs/look_back_progress`, + method: 'POST', + body, + }); + }, + + categorizationFieldExamples( + indexPatternTitle: string, + query: any, + size: number, + field: string, + timeField: string, + start: number, + end: number, + analyzer: CategorizationAnalyzer + ) { + const body = JSON.stringify({ + indexPatternTitle, + query, + size, + field, + timeField, + start, + end, + analyzer, + }); + return http<{ + examples: CategoryFieldExample[]; + sampleSize: number; + overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS; + validationChecks: FieldExampleCheck[]; + }>({ + path: `${basePath()}/jobs/categorization_field_examples`, + method: 'POST', + body, + }); + }, + + topCategories(jobId: string, count: number) { + const body = JSON.stringify({ jobId, count }); + return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({ + path: `${basePath()}/jobs/top_categories`, + method: 'POST', + body, + }); + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js deleted file mode 100644 index e770e80f4c4d9..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// Service for obtaining data for the ML Results dashboards. - -import { http, http$ } from '../http_service'; - -import { basePath } from './index'; - -export const results = { - getAnomaliesTableData( - jobIds, - criteriaFields, - influencers, - aggregationInterval, - threshold, - earliestMs, - latestMs, - dateFormatTz, - maxRecords, - maxExamples, - influencersFilterQuery - ) { - return http$(`${basePath()}/results/anomalies_table_data`, { - method: 'POST', - body: { - jobIds, - criteriaFields, - influencers, - aggregationInterval, - threshold, - earliestMs, - latestMs, - dateFormatTz, - maxRecords, - maxExamples, - influencersFilterQuery, - }, - }); - }, - - getMaxAnomalyScore(jobIds, earliestMs, latestMs) { - return http({ - url: `${basePath()}/results/max_anomaly_score`, - method: 'POST', - data: { - jobIds, - earliestMs, - latestMs, - }, - }); - }, - - getCategoryDefinition(jobId, categoryId) { - return http({ - url: `${basePath()}/results/category_definition`, - method: 'POST', - data: { jobId, categoryId }, - }); - }, - - getCategoryExamples(jobId, categoryIds, maxExamples) { - return http({ - url: `${basePath()}/results/category_examples`, - method: 'POST', - data: { - jobId, - categoryIds, - maxExamples, - }, - }); - }, - - fetchPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs) { - return http$(`${basePath()}/results/partition_fields_values`, { - method: 'POST', - body: { - jobId, - searchTerm, - criteriaFields, - earliestMs, - latestMs, - }, - }); - }, -}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.ts new file mode 100644 index 0000000000000..830e6fab4163a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Service for obtaining data for the ML Results dashboards. +import { http, http$ } from '../http_service'; + +import { basePath } from './index'; + +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; +import { PartitionFieldsDefinition } from '../results_service/result_service_rx'; + +export const results = { + getAnomaliesTableData( + jobIds: string[], + criteriaFields: string[], + influencers: string[], + aggregationInterval: string, + threshold: number, + earliestMs: number, + latestMs: number, + dateFormatTz: string, + maxRecords: number, + maxExamples: number, + influencersFilterQuery: any + ) { + const body = JSON.stringify({ + jobIds, + criteriaFields, + influencers, + aggregationInterval, + threshold, + earliestMs, + latestMs, + dateFormatTz, + maxRecords, + maxExamples, + influencersFilterQuery, + }); + + return http$({ + path: `${basePath()}/results/anomalies_table_data`, + method: 'POST', + body, + }); + }, + + getMaxAnomalyScore(jobIds: string[], earliestMs: number, latestMs: number) { + const body = JSON.stringify({ + jobIds, + earliestMs, + latestMs, + }); + return http({ + path: `${basePath()}/results/max_anomaly_score`, + method: 'POST', + body, + }); + }, + + getCategoryDefinition(jobId: string, categoryId: string) { + const body = JSON.stringify({ jobId, categoryId }); + return http({ + path: `${basePath()}/results/category_definition`, + method: 'POST', + body, + }); + }, + + getCategoryExamples(jobId: string, categoryIds: string[], maxExamples: number) { + const body = JSON.stringify({ + jobId, + categoryIds, + maxExamples, + }); + return http({ + path: `${basePath()}/results/category_examples`, + method: 'POST', + body, + }); + }, + + fetchPartitionFieldsValues( + jobId: JobId, + searchTerm: Record, + criteriaFields: Array<{ fieldName: string; fieldValue: any }>, + earliestMs: number, + latestMs: number + ) { + const body = JSON.stringify({ jobId, searchTerm, criteriaFields, earliestMs, latestMs }); + return http$({ + path: `${basePath()}/results/partition_fields_values`, + method: 'POST', + body, + }); + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts index 304778281c2f2..8ab955b479108 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts @@ -5,21 +5,7 @@ */ import { ml } from './ml_api_service'; -import { CategorizationAnalyzer } from '../../../common/types/categories'; - -export interface MlServerDefaults { - anomaly_detectors: { - categorization_examples_limit?: number; - model_memory_limit?: string; - model_snapshot_retention_days?: number; - categorization_analyzer?: CategorizationAnalyzer; - }; - datafeeds: { scroll_size?: number }; -} - -export interface MlServerLimits { - max_model_memory_limit?: string; -} +import { MlServerDefaults, MlServerLimits } from '../../../common/types/ml_server_info'; export interface CloudInfo { cloudId: string | null; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 978f5f68d9d8d..db5ff2ad91910 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -138,7 +138,7 @@ function getChartDetails( obj.results.entityData.entities = entityFields; resolve(obj); } else { - const entityFieldNames = _.map(blankEntityFields, 'fieldName'); + const entityFieldNames: string[] = _.map(blankEntityFields, 'fieldName'); ml.getCardinalityOfFields({ index: chartConfig.datafeedConfig.indices, fieldNames: entityFieldNames, diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts index 2a1ffe79d033c..f7d524c3a19b7 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimefilterSetup } from 'src/plugins/data/public'; +import { DataPublicPluginSetup } from 'src/plugins/data/public'; import { IUiSettingsClient, ChromeStart, SavedObjectsClientContract, ApplicationStart, HttpStart, -} from 'src/core/public'; +} from 'kibana/public'; import { IndexPatternsContract, DataPublicPluginStart } from 'src/plugins/data/public'; import { DocLinksStart, @@ -23,7 +23,7 @@ import { import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; export interface DependencyCache { - timefilter: TimefilterSetup | null; + timefilter: DataPublicPluginSetup['query']['timefilter'] | null; config: IUiSettingsClient | null; indexPatterns: IndexPatternsContract | null; chrome: ChromeStart | null; diff --git a/x-pack/legacy/plugins/ml/public/index.ts b/x-pack/legacy/plugins/ml/public/index.ts index bafeb7277927f..56fbdb43f34f2 100755 --- a/x-pack/legacy/plugins/ml/public/index.ts +++ b/x-pack/legacy/plugins/ml/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializer } from '../../../../../src/core/public'; +import { PluginInitializer } from 'kibana/public'; import { MlPlugin, Setup, Start } from './plugin'; export const plugin: PluginInitializer = () => new MlPlugin(); diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts index 9fb53e78d9454..8a8ff5a7a9d13 100644 --- a/x-pack/legacy/plugins/ml/public/legacy.ts +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -5,7 +5,7 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext } from 'kibana/public'; import { SecurityPluginSetup } from '../../../../plugins/security/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts index 7b3a5f6fadfac..928f353fd622e 100644 --- a/x-pack/legacy/plugins/ml/public/plugin.ts +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; +import { Plugin, CoreStart, CoreSetup } from 'kibana/public'; import { MlDependencies } from './application/app'; export class MlPlugin implements Plugin { @@ -14,8 +14,8 @@ export class MlPlugin implements Plugin { title: 'Machine learning', async mount(context, params) { const [coreStart, depsStart] = await core.getStartServices(); - const { renderApp } = await import('./application/app'); - return renderApp(coreStart, depsStart, { + const { renderApp: renderMlApp } = await import('./application/app'); + return renderMlApp(coreStart, depsStart, { element: params.element, appBasePath: params.appBasePath, onAppLeave: params.onAppLeave, diff --git a/x-pack/legacy/plugins/rollup/public/legacy.ts b/x-pack/legacy/plugins/rollup/public/legacy.ts index e3e663ac7b0f4..e137799bd34fe 100644 --- a/x-pack/legacy/plugins/rollup/public/legacy.ts +++ b/x-pack/legacy/plugins/rollup/public/legacy.ts @@ -7,7 +7,6 @@ import { npSetup, npStart } from 'ui/new_platform'; import { aggTypeFilters } from 'ui/agg_types'; import { aggTypeFieldFilters } from 'ui/agg_types'; -import { addSearchStrategy } from '../../../../../src/plugins/data/public'; import { RollupPlugin } from './plugin'; import { setup as management } from '../../../../../src/legacy/core_plugins/management/public/legacy'; @@ -18,7 +17,6 @@ export const setup = plugin.setup(npSetup.core, { __LEGACY: { aggTypeFilters, aggTypeFieldFilters, - addSearchStrategy, managementLegacy: management, }, }); diff --git a/x-pack/legacy/plugins/rollup/public/plugin.ts b/x-pack/legacy/plugins/rollup/public/plugin.ts index a01383f4733ef..2d2ff4c8449d8 100644 --- a/x-pack/legacy/plugins/rollup/public/plugin.ts +++ b/x-pack/legacy/plugins/rollup/public/plugin.ts @@ -7,14 +7,12 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { AggTypeFilters, AggTypeFieldFilters } from './legacy_imports'; -import { SearchStrategyProvider } from '../../../../../src/plugins/data/public'; import { ManagementSetup as ManagementSetupLegacy } from '../../../../../src/legacy/core_plugins/management/public/np_ready'; import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_management'; // @ts-ignore import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; // @ts-ignore import { RollupIndexPatternListConfig } from './index_pattern_list/rollup_index_pattern_list_config'; -import { getRollupSearchStrategy } from './search/rollup_search_strategy'; // @ts-ignore import { initAggTypeFilter } from './visualize/agg_type_filter'; // @ts-ignore @@ -37,7 +35,6 @@ export interface RollupPluginSetupDependencies { __LEGACY: { aggTypeFilters: AggTypeFilters; aggTypeFieldFilters: AggTypeFieldFilters; - addSearchStrategy: (searchStrategy: SearchStrategyProvider) => void; managementLegacy: ManagementSetupLegacy; }; home?: HomePublicPluginSetup; @@ -49,7 +46,7 @@ export class RollupPlugin implements Plugin { setup( core: CoreSetup, { - __LEGACY: { aggTypeFilters, aggTypeFieldFilters, addSearchStrategy, managementLegacy }, + __LEGACY: { aggTypeFilters, aggTypeFieldFilters, managementLegacy }, home, management, indexManagement, @@ -67,7 +64,6 @@ export class RollupPlugin implements Plugin { if (isRollupIndexPatternsEnabled) { managementLegacy.indexPattern.creation.add(RollupIndexPatternCreationConfig); managementLegacy.indexPattern.list.add(RollupIndexPatternListConfig); - addSearchStrategy(getRollupSearchStrategy(core.http.fetch)); initAggTypeFilter(aggTypeFilters); initAggTypeFieldFilter(aggTypeFieldFilters); } diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index 2513004af84dd..4dd1b114ccff3 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -11,8 +11,14 @@ import { Dispatch } from 'redux'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { FilterManager, IIndexPattern, TimeRange, Query, Filter } from 'src/plugins/data/public'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; +import { + FilterManager, + IIndexPattern, + TimeRange, + Query, + Filter, + SavedQuery, +} from 'src/plugins/data/public'; import { OnTimeChangeProps } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts b/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts index f501466db9ed9..793737a1ad754 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts @@ -5,9 +5,8 @@ */ import { createSelector } from 'reselect'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { InputsRange } from '../../store/inputs/model'; -import { Query } from '../../../../../../../src/plugins/data/public'; +import { Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; export { endSelector, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 89190afabef9f..ce98dd3573d30 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -119,3 +119,11 @@ export const patchComment = async ( ); return convertToCamelCase(decodeCommentResponse(response)); }; + +export const deleteCases = async (caseIds: string[]): Promise => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + }); + return response === 'true' ? true : false; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index ac62ba7b6f997..a0e57faa7661f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -7,11 +7,3 @@ export const CASES_URL = `/api/cases`; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; -export const FETCH_FAILURE = 'FETCH_FAILURE'; -export const FETCH_INIT = 'FETCH_INIT'; -export const FETCH_SUCCESS = 'FETCH_SUCCESS'; -export const POST_NEW_CASE = 'POST_NEW_CASE'; -export const POST_NEW_COMMENT = 'POST_NEW_COMMENT'; -export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; -export const UPDATE_TABLE_SELECTIONS = 'UPDATE_TABLE_SELECTIONS'; -export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index d479abdbd4489..c89993ec67179 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -16,6 +16,7 @@ export interface Comment { export interface Case { id: string; comments: Comment[]; + commentIds: string[]; createdAt: string; createdBy: ElasticUser; description: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx new file mode 100644 index 0000000000000..d5a3b3cf9314c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useReducer } from 'react'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { deleteCases } from './api'; + +interface DeleteState { + isDisplayConfirmDeleteModal: boolean; + isDeleted: boolean; + isLoading: boolean; + isError: boolean; +} +type Action = + | { type: 'DISPLAY_MODAL'; payload: boolean } + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: boolean } + | { type: 'FETCH_FAILURE' } + | { type: 'RESET_IS_DELETED' }; + +const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { + switch (action.type) { + case 'DISPLAY_MODAL': + return { + ...state, + isDisplayConfirmDeleteModal: action.payload, + }; + case 'FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + }; + case 'FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + isDeleted: action.payload, + }; + case 'FETCH_FAILURE': + return { + ...state, + isLoading: false, + isError: true, + }; + case 'RESET_IS_DELETED': + return { + ...state, + isDeleted: false, + }; + default: + return state; + } +}; +interface UseDeleteCase extends DeleteState { + dispatchResetIsDeleted: () => void; + handleOnDeleteConfirm: (caseIds: string[]) => void; + handleToggleModal: () => void; +} + +export const useDeleteCases = (): UseDeleteCase => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: false, + isDeleted: false, + }); + const [, dispatchToaster] = useStateToaster(); + + const dispatchDeleteCases = useCallback((caseIds: string[]) => { + let cancel = false; + const deleteData = async () => { + try { + dispatch({ type: 'FETCH_INIT' }); + await deleteCases(caseIds); + if (!cancel) { + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + } + } catch (error) { + if (!cancel) { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + dispatch({ type: 'FETCH_FAILURE' }); + } + } + }; + deleteData(); + return () => { + cancel = true; + }; + }, []); + + const dispatchToggleDeleteModal = useCallback(() => { + dispatch({ type: 'DISPLAY_MODAL', payload: !state.isDisplayConfirmDeleteModal }); + }, [state.isDisplayConfirmDeleteModal]); + + const dispatchResetIsDeleted = useCallback(() => { + dispatch({ type: 'RESET_IS_DELETED' }); + }, [state.isDisplayConfirmDeleteModal]); + + const handleOnDeleteConfirm = useCallback( + caseIds => { + dispatchDeleteCases(caseIds); + dispatchToggleDeleteModal(); + }, + [state.isDisplayConfirmDeleteModal] + ); + const handleToggleModal = useCallback(() => { + dispatchToggleDeleteModal(); + }, [state.isDisplayConfirmDeleteModal]); + + return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index b758f914c991e..6020969ed6375 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -7,8 +7,6 @@ import { useEffect, useReducer } from 'react'; import { Case } from './types'; -import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; -import { getTypedPayload } from './utils'; import * as i18n from './translations'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getCase } from './api'; @@ -18,40 +16,42 @@ interface CaseState { isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: Case; -} + +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: CaseState, action: Action): CaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - data: getTypedPayload(action.payload), + data: action.payload, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; const initialData: Case = { id: '', createdAt: '', comments: [], + commentIds: [], createdBy: { username: '', }, @@ -63,7 +63,7 @@ const initialData: Case = { version: '', }; -export const useGetCase = (caseId: string): [CaseState] => { +export const useGetCase = (caseId: string): CaseState => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: true, isError: false, @@ -74,11 +74,11 @@ export const useGetCase = (caseId: string): [CaseState] => { const callFetch = () => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); try { const response = await getCase(caseId); if (!didCancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!didCancel) { @@ -87,7 +87,7 @@ export const useGetCase = (caseId: string): [CaseState] => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } }; @@ -100,5 +100,5 @@ export const useGetCase = (caseId: string): [CaseState] => { useEffect(() => { callFetch(); }, [caseId]); - return [state]; + return state; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 99c7ef0c757c7..1c7c30ae9da18 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -96,7 +96,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS selectedCases: action.payload, }; default: - throw new Error(); + return state; } }; @@ -109,6 +109,7 @@ const initialData: AllCases = { interface UseGetCases extends UseGetCasesState { dispatchUpdateCaseProperty: ({ updateKey, updateValue, caseId, version }: UpdateCase) => void; getCaseCount: (caseState: keyof CaseCount) => void; + refetchCases: (filters: FilterOptions, queryParams: QueryParams) => void; setFilters: (filters: FilterOptions) => void; setQueryParams: (queryParams: QueryParams) => void; setSelectedCases: (mySelectedCases: Case[]) => void; @@ -245,10 +246,17 @@ export const useGetCases = (): UseGetCases => { [state.filterOptions, state.queryParams] ); + const refetchCases = useCallback(() => { + fetchCases(state.filterOptions, state.queryParams); + getCaseCount('open'); + getCaseCount('closed'); + }, [state.filterOptions, state.queryParams]); + return { ...state, dispatchUpdateCaseProperty, getCaseCount, + refetchCases, setFilters, setQueryParams, setSelectedCases, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx index 5e6df9b92f462..e3657f5b09da9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -8,63 +8,61 @@ import { useEffect, useReducer } from 'react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getTags } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; interface TagsState { - data: string[]; + tags: string[]; isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: string[]; -} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: string[] } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: TagsState, action: Action): TagsState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: - const getTypedPayload = (a: Action['payload']) => a as string[]; + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - data: getTypedPayload(action.payload), + tags: action.payload, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; const initialData: string[] = []; -export const useGetTags = (): [TagsState] => { +export const useGetTags = (): TagsState => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, - data: initialData, + tags: initialData, }); const [, dispatchToaster] = useStateToaster(); useEffect(() => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); try { const response = await getTags(); if (!didCancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!didCancel) { @@ -73,7 +71,7 @@ export const useGetTags = (): [TagsState] => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } }; @@ -82,5 +80,5 @@ export const useGetTags = (): [TagsState] => { didCancel = true; }; }, []); - return [state]; + return state; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 5cd0911fae81a..14b9e78846906 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -9,7 +9,6 @@ import { useReducer, useCallback } from 'react'; import { CaseRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postCase } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Case } from './types'; @@ -18,34 +17,34 @@ interface NewCaseState { isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: Case; -} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, caseData: action.payload ?? null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; @@ -63,11 +62,11 @@ export const usePostCase = (): UsePostCase => { const postMyCase = useCallback(async (data: CaseRequest) => { let cancel = false; try { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); const response = await postCase({ ...data, state: 'open' }); if (!cancel) { dispatch({ - type: FETCH_SUCCESS, + type: 'FETCH_SUCCESS', payload: response, }); } @@ -78,7 +77,7 @@ export const usePostCase = (): UsePostCase => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx index 1467c691f547e..a96cb97d7cc7b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -10,7 +10,6 @@ import { CommentRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postComment } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Comment } from './types'; @@ -20,39 +19,46 @@ interface NewCommentState { isError: boolean; caseId: string; } -interface Action { - type: string; - payload?: Comment; -} +type Action = + | { type: 'RESET_COMMENT_DATA' } + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Comment } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentState => { switch (action.type) { - case FETCH_INIT: + case 'RESET_COMMENT_DATA': + return { + ...state, + commentData: null, + }; + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, commentData: action.payload ?? null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; interface UsePostComment extends NewCommentState { postComment: (data: CommentRequest) => void; + resetCommentData: () => void; } export const usePostComment = (caseId: string): UsePostComment => { @@ -67,10 +73,10 @@ export const usePostComment = (caseId: string): UsePostComment => { const postMyComment = useCallback(async (data: CommentRequest) => { let cancel = false; try { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); const response = await postComment(data, state.caseId); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!cancel) { @@ -79,7 +85,7 @@ export const usePostComment = (caseId: string): UsePostComment => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { @@ -87,5 +93,7 @@ export const usePostComment = (caseId: string): UsePostComment => { }; }, []); - return { ...state, postComment: postMyComment }; + const resetCommentData = useCallback(() => dispatch({ type: 'RESET_COMMENT_DATA' }), []); + + return { ...state, postComment: postMyComment, resetCommentData }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 594677aefe245..2b1081b9b901c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -10,10 +10,8 @@ import { CaseRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Case } from './types'; -import { getTypedPayload } from './utils'; type UpdateKey = keyof CaseRequest; @@ -29,30 +27,30 @@ export interface UpdateByKey { updateValue: CaseRequest[UpdateKey]; } -interface Action { - type: string; - payload?: Case | UpdateKey; -} +type Action = + | { type: 'FETCH_INIT'; payload: UpdateKey } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, - updateKey: getTypedPayload(action.payload), + updateKey: action.payload, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - caseData: getTypedPayload(action.payload), + caseData: action.payload, updateKey: null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, @@ -60,7 +58,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => updateKey: null, }; default: - throw new Error(); + return state; } }; @@ -80,14 +78,14 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase async ({ updateKey, updateValue }: UpdateByKey) => { let cancel = false; try { - dispatch({ type: FETCH_INIT, payload: updateKey }); + dispatch({ type: 'FETCH_INIT', payload: updateKey }); const response = await patchCase( caseId, { [updateKey]: updateValue }, state.caseData.version ); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!cancel) { @@ -96,7 +94,7 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx index 0e39d2303a32a..a40a1100ca735 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useReducer, useCallback } from 'react'; +import { useReducer, useCallback, Dispatch } from 'react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchComment } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Comment } from './types'; -import { getTypedPayload } from './utils'; interface CommentUpdateState { comments: Comment[]; @@ -25,22 +23,28 @@ interface CommentUpdate { commentId: string; } -interface Action { - type: string; - payload?: CommentUpdate | string; -} +type Action = + | { type: 'APPEND_COMMENT'; payload: Comment } + | { type: 'FETCH_INIT'; payload: string } + | { type: 'FETCH_SUCCESS'; payload: CommentUpdate } + | { type: 'FETCH_FAILURE'; payload: string }; const dataFetchReducer = (state: CommentUpdateState, action: Action): CommentUpdateState => { switch (action.type) { - case FETCH_INIT: + case 'APPEND_COMMENT': return { ...state, - isLoadingIds: [...state.isLoadingIds, getTypedPayload(action.payload)], + comments: [...state.comments, action.payload], + }; + case 'FETCH_INIT': + return { + ...state, + isLoadingIds: [...state.isLoadingIds, action.payload], isError: false, }; - case FETCH_SUCCESS: - const updatePayload = getTypedPayload(action.payload); + case 'FETCH_SUCCESS': + const updatePayload = action.payload; const foundIndex = state.comments.findIndex( comment => comment.id === updatePayload.commentId ); @@ -55,21 +59,20 @@ const dataFetchReducer = (state: CommentUpdateState, action: Action): CommentUpd isError: false, comments: newComments, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, - isLoadingIds: state.isLoadingIds.filter( - id => getTypedPayload(action.payload) !== id - ), + isLoadingIds: state.isLoadingIds.filter(id => action.payload !== id), isError: true, }; default: - throw new Error(); + return state; } }; interface UseUpdateComment extends CommentUpdateState { updateComment: (caseId: string, commentId: string, commentUpdate: string) => void; + addPostedComment: Dispatch; } export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { @@ -84,7 +87,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { async (caseId: string, commentId: string, commentUpdate: string) => { let cancel = false; try { - dispatch({ type: FETCH_INIT, payload: commentId }); + dispatch({ type: 'FETCH_INIT', payload: commentId }); const currentComment = state.comments.find(comment => comment.id === commentId) ?? { version: '', }; @@ -95,7 +98,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { currentComment.version ); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: { update: response, commentId } }); + dispatch({ type: 'FETCH_SUCCESS', payload: { update: response, commentId } }); } } catch (error) { if (!cancel) { @@ -104,7 +107,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE, payload: commentId }); + dispatch({ type: 'FETCH_FAILURE', payload: commentId }); } } return () => { @@ -113,6 +116,10 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { }, [state] ); + const addPostedComment = useCallback( + (comment: Comment) => dispatch({ type: 'APPEND_COMMENT', payload: comment }), + [] + ); - return { ...state, updateComment: dispatchUpdateComment }; + return { ...state, updateComment: dispatchUpdateComment, addPostedComment }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx index 6b63961b4194f..0b3b0daaf4bbc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { CommentRequest } from '../../../../../../../../plugins/case/common/api'; @@ -16,6 +16,7 @@ import * as i18n from '../../translations'; import { schema } from './schema'; import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { Comment } from '../../../../containers/case/types'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; @@ -27,10 +28,13 @@ const initialCommentValue: CommentRequest = { comment: '', }; -export const AddComment = React.memo<{ +interface AddCommentProps { caseId: string; -}>(({ caseId }) => { - const { commentData, isLoading, postComment } = usePostComment(caseId); + onCommentPosted: (commentResponse: Comment) => void; +} + +export const AddComment = React.memo(({ caseId, onCommentPosted }) => { + const { commentData, isLoading, postComment, resetCommentData } = usePostComment(caseId); const { form } = useForm({ defaultValue: initialCommentValue, options: { stripEmptyFields: false }, @@ -40,6 +44,15 @@ export const AddComment = React.memo<{ form, 'comment' ); + + useEffect(() => { + if (commentData !== null) { + onCommentPosted(commentData); + form.reset(); + resetCommentData(); + } + }, [commentData]); + const onSubmit = useCallback(async () => { const { isValid, data } = await form.submit(); if (isValid) { @@ -81,8 +94,6 @@ export const AddComment = React.memo<{ }} /> - {commentData != null && - 'TO DO new comment got added but we didnt update the UI yet. Refresh the page to see your comment ;)'} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 2e57e5f2f95d9..bc6dfe4af25ff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -14,6 +14,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -26,6 +27,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '362a5c10-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:13.328Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -38,6 +40,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:11.328Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -50,6 +53,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '31890e90-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:05.563Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'closed', @@ -62,6 +66,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '2f5b3210-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:01.901Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx index 0ec09f2b57918..33a1953b9d2f8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx @@ -13,18 +13,19 @@ import { UpdateCase } from '../../../../containers/case/use_get_cases'; interface GetActions { caseStatus: string; dispatchUpdate: Dispatch; + deleteCaseOnClick: (deleteCase: Case) => void; } export const getActions = ({ caseStatus, dispatchUpdate, + deleteCaseOnClick, }: GetActions): Array> => [ { description: i18n.DELETE, icon: 'trash', name: i18n.DELETE, - // eslint-disable-next-line no-console - onClick: ({ id }: Case) => console.log('TO DO Delete case', id), + onClick: deleteCaseOnClick, type: 'icon', 'data-test-subj': 'action-delete', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index f6ed2694fdc40..db3313d843547 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -33,9 +33,8 @@ const Spacer = styled.span` margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const TempNumberComponent = () => {1}; -TempNumberComponent.displayName = 'TempNumberComponent'; - +const renderStringField = (field: string, dataTestSubj: string) => + field != null ? {field} : getEmptyTagValue(); export const getCasesColumns = ( actions: Array> ): CasesColumns[] => [ @@ -59,6 +58,7 @@ export const getCasesColumns = ( } return getEmptyTagValue(); }, + width: '25%', }, { field: 'createdBy', @@ -101,13 +101,15 @@ export const getCasesColumns = ( return getEmptyTagValue(); }, truncateText: true, + width: '20%', }, { align: 'right', - field: 'commentCount', // TO DO once we have commentCount returned in the API: https://github.com/elastic/kibana/issues/58525 + field: 'commentIds', name: i18n.COMMENTS, sortable: true, - render: TempNumberComponent, + render: (comments: Case['commentIds']) => + renderStringField(`${comments.length}`, `case-table-column-commentCount`), }, { field: 'createdAt', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index a9dd15086df27..10786940eee7f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -15,17 +15,19 @@ import { act } from '@testing-library/react'; import { wait } from '../../../../lib/helpers'; describe('AllCases', () => { + const dispatchUpdateCaseProperty = jest.fn(); + const getCaseCount = jest.fn(); + const refetchCases = jest.fn(); const setFilters = jest.fn(); const setQueryParams = jest.fn(); const setSelectedCases = jest.fn(); - const getCaseCount = jest.fn(); - const dispatchUpdateCaseProperty = jest.fn(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(apiHook, 'useGetCases').mockReturnValue({ ...useGetCasesMockState, dispatchUpdateCaseProperty, getCaseCount, + refetchCases, setFilters, setQueryParams, setSelectedCases, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 484d9051ee43f..1d22f6a246960 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -25,6 +25,7 @@ import { getCasesColumns } from './columns'; import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; import { CasesTableFilters } from './table_filters'; @@ -41,6 +42,7 @@ import { getBulkItems } from '../bulk_actions'; import { CaseHeaderPage } from '../case_header_page'; import { OpenClosedStats } from '../open_closed_stats'; import { getActions } from './actions'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -57,11 +59,9 @@ const FlexItemDivider = styled(EuiFlexItem)` const ProgressLoader = styled(EuiProgress)` ${({ theme }) => css` - .euiFlexGroup--gutterMedium > &.euiFlexItem { - top: 2px; - border-radius: ${theme.eui.euiBorderRadius}; - z-index: ${theme.eui.euiZHeader}; - } + top: 2px; + border-radius: ${theme.eui.euiBorderRadius}; + z-index: ${theme.eui.euiZHeader}; `} `; @@ -83,11 +83,95 @@ export const AllCases = React.memo(() => { loading, queryParams, selectedCases, + refetchCases, setFilters, setQueryParams, setSelectedCases, } = useGetCases(); + // Delete case + const { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isLoading: isDeleting, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + + useEffect(() => { + if (isDeleted) { + refetchCases(filterOptions, queryParams); + dispatchResetIsDeleted(); + } + }, [isDeleted, filterOptions, queryParams]); + + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); + const confirmDeleteModal = useMemo( + () => ( + 0} + onCancel={handleToggleModal} + onConfirm={handleOnDeleteConfirm.bind( + null, + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id] + )} + /> + ), + [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] + ); + + const toggleDeleteModal = useCallback( + (deleteCase: Case) => { + handleToggleModal(); + setDeleteThisCase(deleteCase); + }, + [isDisplayConfirmDeleteModal] + ); + + const toggleBulkDeleteModal = useCallback( + (deleteCases: string[]) => { + handleToggleModal(); + setDeleteBulk(deleteCases); + }, + [isDisplayConfirmDeleteModal] + ); + + const selectedCaseIds = useMemo( + (): string[] => + selectedCases.reduce((arr: string[], caseObj: Case) => [...arr, caseObj.id], []), + [selectedCases] + ); + + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedCaseIds, filterOptions.state] + ); + const actions = useMemo( + () => + getActions({ + caseStatus: filterOptions.state, + deleteCaseOnClick: toggleDeleteModal, + dispatchUpdate: dispatchUpdateCaseProperty, + }), + [filterOptions.state] + ); + const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; @@ -117,12 +201,6 @@ export const AllCases = React.memo(() => { [filterOptions, setFilters] ); - const actions = useMemo( - () => - getActions({ caseStatus: filterOptions.state, dispatchUpdate: dispatchUpdateCaseProperty }), - [filterOptions.state, dispatchUpdateCaseProperty] - ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [filterOptions.state]); const memoizedPagination = useMemo( () => ({ @@ -134,19 +212,6 @@ export const AllCases = React.memo(() => { [data, queryParams] ); - const getBulkItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedCases, filterOptions.state] - ); - const sorting: EuiTableSortingType = { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; @@ -162,7 +227,6 @@ export const AllCases = React.memo(() => { [loading] ); const isDataEmpty = useMemo(() => data.total === 0, [data]); - return ( <> @@ -197,7 +261,9 @@ export const AllCases = React.memo(() => { - {isCasesLoading && !isDataEmpty && } + {(isCasesLoading || isDeleting) && !isDataEmpty && ( + + )} { - {i18n.SELECTED_CASES(selectedCases.length)} + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} { { )} + {confirmDeleteModal} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index 5256fb6d7b3ee..9356577fd1888 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -38,7 +38,7 @@ const CasesTableFiltersComponent = ({ const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [showOpenCases, setShowOpenCases] = useState(initial.state === 'open'); - const [{ data }] = useGetTags(); + const { tags } = useGetTags(); const handleSelectedTags = useCallback( newTags => { @@ -106,7 +106,7 @@ const CasesTableFiltersComponent = ({ buttonLabel={i18n.TAGS} onSelectedOptionsChanged={handleSelectedTags} selectedOptions={selectedTags} - options={data} + options={tags} optionsEmptyLabel={i18n.NO_TAGS_AVAILABLE} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index 19117136ed046..27532e57166e1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -18,7 +18,7 @@ export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase defaultMessage: 'Add New Case', }); -export const SELECTED_CASES = (totalRules: number) => +export const SHOWING_SELECTED_CASES = (totalRules: number) => i18n.translate('xpack.siem.case.caseTable.selectedCasesTitle', { values: { totalRules }, defaultMessage: 'Selected {totalRules} {totalRules, plural, =1 {case} other {cases}}', @@ -66,6 +66,3 @@ export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase' export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { defaultMessage: 'Close case', }); -export const DUPLICATE_CASE = i18n.translate('xpack.siem.case.caseTable.duplicateCase', { - defaultMessage: 'Duplicate case', -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx index 2fe25a7d1f5d0..f171ebf91b787 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -4,29 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; import * as i18n from './translations'; -import { Case } from '../../../../containers/case/types'; interface GetBulkItems { - // cases: Case[]; closePopover: () => void; - // dispatch: Dispatch; - // dispatchToaster: Dispatch; - // reFetchCases: (refreshPrePackagedCase?: boolean) => void; - selectedCases: Case[]; + deleteCasesAction: (cases: string[]) => void; + selectedCaseIds: string[]; caseStatus: string; } export const getBulkItems = ({ - // cases, + deleteCasesAction, closePopover, caseStatus, - // dispatch, - // dispatchToaster, - // reFetchCases, - selectedCases, + selectedCaseIds, }: GetBulkItems) => { return [ caseStatus === 'open' ? ( @@ -36,8 +29,6 @@ export const getBulkItems = ({ disabled={true} // TO DO onClick={async () => { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); }} > {i18n.BULK_ACTION_CLOSE_SELECTED} @@ -47,10 +38,8 @@ export const getBulkItems = ({ key={i18n.BULK_ACTION_OPEN_SELECTED} icon="magnet" disabled={true} // TO DO - onClick={async () => { + onClick={() => { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); }} > {i18n.BULK_ACTION_OPEN_SELECTED} @@ -59,11 +48,9 @@ export const getBulkItems = ({ { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); + deleteCasesAction(selectedCaseIds); }} > {i18n.BULK_ACTION_DELETE_SELECTED} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx index c2d3cae6774b0..3875c316e80d1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -11,6 +11,7 @@ export const caseProps: CaseProps = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', initialData: { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], comments: [ { comment: 'Solve this fast!', @@ -37,6 +38,7 @@ export const caseProps: CaseProps = { export const data: Case = { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], comments: [ { comment: 'Solve this fast!', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index c917d27aebea3..080cbdc143593 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiBadge, EuiButtonToggle, @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import { Redirect } from 'react-router-dom'; import * as i18n from './translations'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -32,6 +33,9 @@ import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +import { SiemPageName } from '../../../home/types'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; interface Props { caseId: string; @@ -62,6 +66,7 @@ export interface CaseProps { export const CaseComponent = React.memo(({ caseId, initialData }) => { const { caseData, isLoading, updateKey, updateCaseProperty } = useUpdateCase(caseId, initialData); + // Update Fields const onUpdateField = useCallback( (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { switch (newUpdateKey) { @@ -104,13 +109,39 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => }, [updateCaseProperty, caseData.state] ); + const toggleStateCase = useCallback( + e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), + [onUpdateField] + ); + const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); + const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + + // Delete case + const { + handleToggleModal, + handleOnDeleteConfirm, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + const confirmDeleteModal = useMemo( + () => ( + + ), + [isDisplayConfirmDeleteModal] + ); // TO DO refactor each of these const's into their own components const propertyActions = [ { iconType: 'trash', label: 'Delete case', - onClick: () => null, + onClick: handleToggleModal, }, { iconType: 'popout', @@ -124,12 +155,9 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => }, ]; - const onSubmit = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); - const toggleStateCase = useCallback( - e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), - [onUpdateField] - ); - const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + if (isDeleted) { + return ; + } return ( <> @@ -144,7 +172,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => } title={caseData.title} @@ -222,12 +250,13 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => + {confirmDeleteModal} ); }); export const CaseView = React.memo(({ caseId }: Props) => { - const [{ data, isLoading, isError }] = useGetCase(caseId); + const { data, isLoading, isError } = useGetCase(caseId); if (isError) { return null; } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx new file mode 100644 index 0000000000000..dff36a6dac571 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import * as i18n from './translations'; + +interface ConfirmDeleteCaseModalProps { + caseTitle: string; + isModalVisible: boolean; + isPlural: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +const ConfirmDeleteCaseModalComp: React.FC = ({ + caseTitle, + isModalVisible, + isPlural, + onCancel, + onConfirm, +}) => { + if (!isModalVisible) { + return null; + } + return ( + + + {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION} + + + ); +}; + +export const ConfirmDeleteCaseModal = React.memo(ConfirmDeleteCaseModalComp); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts new file mode 100644 index 0000000000000..06e940c60d0a1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +export * from '../../translations'; + +export const DELETE_TITLE = (caseTitle: string) => + i18n.translate('xpack.siem.case.confirmDeleteCase.deleteTitle', { + values: { caseTitle }, + defaultMessage: 'Delete "{caseTitle}"', + }); + +export const CONFIRM_QUESTION = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.confirmQuestion', + { + defaultMessage: + 'By deleting this case, all related case data will be permanently removed and you will no longer be able to push data to a third-party case management system. Are you sure you wish to proceed?', + } +); +export const DELETE_SELECTED_CASES = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.selectedCases', + { + defaultMessage: 'Delete selected cases', + } +); + +export const CONFIRM_QUESTION_PLURAL = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.confirmQuestionPlural', + { + defaultMessage: + 'By deleting these cases, all related case data will be permanently removed and you will no longer be able to push data to a third-party case management system. Are you sure you wish to proceed?', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index d9c4175b2d2bd..cebc66a0c8363 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -24,7 +24,9 @@ const NewId = 'newComent'; export const UserActionTree = React.memo( ({ data: caseData, onUpdateField, isLoadingDescription }: UserActionTreeProps) => { - const { comments, isLoadingIds, updateComment } = useUpdateComment(caseData.comments); + const { comments, isLoadingIds, updateComment, addPostedComment } = useUpdateComment( + caseData.comments + ); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); @@ -63,7 +65,10 @@ export const UserActionTree = React.memo( [caseData.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] ); - const MarkdownNewComment = useMemo(() => , [caseData.id]); + const MarkdownNewComment = useMemo( + () => , + [caseData.id] + ); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index d9e4c2725cb10..9c0287a56ccbc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -14,6 +14,14 @@ export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { defaultMessage: 'Cancel', }); +export const DELETE_CASE = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCase', { + defaultMessage: 'Delete case', +}); + +export const DELETE_CASES = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCases', { + defaultMessage: 'Delete cases', +}); + export const NAME = i18n.translate('xpack.siem.case.caseView.name', { defaultMessage: 'Name', }); @@ -64,26 +72,10 @@ export const OPTIONAL = i18n.translate('xpack.siem.case.caseView.optional', { defaultMessage: 'Optional', }); -export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', { - defaultMessage: 'Last updated', -}); - -export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { - defaultMessage: 'Cases within the Elastic SIEM', -}); - export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { defaultMessage: 'Cases', }); -export const STATE = i18n.translate('xpack.siem.case.caseView.state', { - defaultMessage: 'State', -}); - -export const SUBMIT = i18n.translate('xpack.siem.case.caseView.submit', { - defaultMessage: 'Submit', -}); - export const CREATE_CASE = i18n.translate('xpack.siem.case.caseView.createCase', { defaultMessage: 'Create case', }); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts b/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts index f9da0e558c655..5b26957843f08 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts @@ -6,10 +6,9 @@ import actionCreatorFactory from 'typescript-fsa'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { InspectQuery, Refetch, RefetchKql } from './model'; import { InputsModelId } from './constants'; -import { Filter } from '../../../../../../../src/plugins/data/public'; +import { Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts index dab6ef3113df0..04facf3b98c3b 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts @@ -5,11 +5,10 @@ */ import { Dispatch } from 'redux'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { Omit } from '../../../common/utility_types'; import { InputsModelId } from './constants'; import { CONSTANTS } from '../../components/url_state/constants'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; export interface AbsoluteTimeRange { kind: 'absolute'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 3c1a01fd58c60..54ed42a1d2b6c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -59,6 +59,7 @@ export const mockPrepackagedRule = (): PrepackagedRules => ({ version: 1, false_positives: [], max_signals: 100, + note: '', timeline_id: 'timeline-id', timeline_title: 'timeline-title', }); @@ -392,6 +393,7 @@ export const getResult = (): RuleAlertType => ({ }, ], references: ['http://www.example.com', 'https://ww.example.com'], + note: '# Investigative notes', version: 1, }, createdAt: new Date('2019-12-13T16:40:33.400Z'), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json index 714b39d1557a1..dc20f0793a6f8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json @@ -133,6 +133,9 @@ } } }, + "note": { + "type": "text" + }, "type": { "type": "keyword" }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index ee8539faacf3e..d727bbb953d2a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -78,6 +78,7 @@ export const createRulesBulkRoute = (router: IRouter) => { to, type, references, + note, timeline_id: timelineId, timeline_title: timelineTitle, version, @@ -131,6 +132,7 @@ export const createRulesBulkRoute = (router: IRouter) => { type, threat, references, + note, version, }); return transformValidateBulkError(ruleIdOrUuid, createdRule); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index cef7ded2b50b4..fcfcee99f369e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -55,6 +55,7 @@ export const createRulesRoute = (router: IRouter): void => { to, type, references, + note, } = request.body; const siemResponse = buildSiemResponse(response); @@ -117,6 +118,7 @@ export const createRulesRoute = (router: IRouter): void => { type, threat, references, + note, version: 1, }); const ruleStatuses = await savedObjectsClient.find< diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index d9fc9b4e3c04f..ec4e707f46e50 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -134,6 +134,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config to, type, references, + note, timeline_id: timelineId, timeline_title: timelineTitle, version, @@ -183,6 +184,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config type, threat, references, + note, version, }); resolve({ rule_id: ruleId, status_code: 200 }); @@ -217,6 +219,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config type, threat, references, + note, version, }); resolve({ rule_id: ruleId, status_code: 200 }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 7ca16a75fb562..e64bbe625f5f6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -71,6 +71,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { type, threat, references, + note, version, } = payloadRule; const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; @@ -104,6 +105,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { type, threat, references, + note, version, }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index dce5f4037db1c..2d810d33c6e51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -55,6 +55,7 @@ export const patchRulesRoute = (router: IRouter) => { type, threat, references, + note, version, } = request.body; const siemResponse = buildSiemResponse(response); @@ -101,6 +102,7 @@ export const patchRulesRoute = (router: IRouter) => { type, threat, references, + note, version, }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 953fb16d26ac6..777b9f3cc7a9d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -72,6 +72,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { type, threat, references, + note, version, } = payloadRule; const finalIndex = outputIndex ?? siemClient.signalsIndex; @@ -107,6 +108,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { type, threat, references, + note, version, }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index fbb930d780f01..1393de8c725cb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -55,6 +55,7 @@ export const updateRulesRoute = (router: IRouter) => { type, threat, references, + note, version, } = request.body; const siemResponse = buildSiemResponse(response); @@ -103,6 +104,7 @@ export const updateRulesRoute = (router: IRouter) => { type, threat, references, + note, version, }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 25f0151923e2e..70fcbb2c163ca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -92,6 +92,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(rule).toEqual(expected); @@ -154,6 +155,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(omitData).toEqual(expected); @@ -218,6 +220,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(rule).toEqual(expected); @@ -282,6 +285,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(rule).toEqual(expected); @@ -344,6 +348,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(omitData).toEqual(expected); @@ -409,6 +414,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(ruleWithEnabledFalse).toEqual(expected); @@ -474,6 +480,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(ruleWithEnabledFalse).toEqual(expected); @@ -539,6 +546,7 @@ describe('utils', () => { timeline_title: 'some-timeline-title', to: 'now', type: 'query', + note: '# Investigative notes', version: 1, }; expect(rule).toEqual(expected); @@ -688,6 +696,7 @@ describe('utils', () => { }, timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', + note: '# Investigative notes', version: 1, }; expect(output).toEqual({ @@ -769,6 +778,7 @@ describe('utils', () => { }, timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', + note: '# Investigative notes', version: 1, }; expect(output).toEqual(expected); @@ -941,6 +951,7 @@ describe('utils', () => { }, timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', + note: '# Investigative notes', version: 1, }; expect(output).toEqual(expected); @@ -1053,6 +1064,7 @@ describe('utils', () => { type: 'query', updated_at: '2019-12-13T16:40:33.400Z', updated_by: 'elastic', + note: '# Investigative notes', version: 1, }, ]); @@ -1112,6 +1124,7 @@ describe('utils', () => { type: 'query', updated_at: '2019-12-13T16:40:33.400Z', updated_by: 'elastic', + note: '# Investigative notes', version: 1, }, { @@ -1160,6 +1173,7 @@ describe('utils', () => { type: 'query', updated_at: '2019-12-13T16:40:33.400Z', updated_by: 'elastic', + note: '# Investigative notes', version: 1, }, ]); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 064bd8315969e..ecf669b0106c3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -131,6 +131,7 @@ export const transformAlertToRule = ( to: alert.params.to, type: alert.params.type, threat: alert.params.threat, + note: alert.params.note, version: alert.params.version, status: ruleStatus?.attributes.status, status_date: ruleStatus?.attributes.statusDate, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 812552aef0ed8..ba6c702e9601b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -72,6 +72,7 @@ export const ruleOutput: RulesSchema = { meta: { someMeta: 'someField', }, + note: '# Investigative notes', timeline_title: 'some-timeline-title', timeline_id: 'some-timeline-id', }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index b536cfac05df3..a002cc9324012 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -1274,4 +1274,62 @@ describe('add prepackaged rules schema', () => { 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' ); }); + + describe('note', () => { + test('You can set note to any string you want', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + note: '# test header', + version: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot create note as anything other than a string', () => { + expect( + addPrepackagedRulesSchema.validate< + Partial & { note: object }> + >({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + note: { + somethingMadeUp: { somethingElse: true }, + }, + version: 1, + }).error.message + ).toEqual('child "note" fails because ["note" must be a string]'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index b62c480492c84..974ddcf35eeb4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -32,6 +32,7 @@ import { type, threat, references, + note, version, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -79,5 +80,6 @@ export const addPrepackagedRulesSchema = Joi.object({ type: type.required(), threat: threat.default([]), references: references.default([]), + note: note.allow(''), version: version.required(), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts index 2a64478962ced..6512bfdc4361f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts @@ -141,4 +141,71 @@ describe('create_rules_bulk_schema', () => { '"value" at position 0 fails because [child "severity" fails because ["severity" must be one of [low, medium, high, critical]]]' ); }); + + test('You can set "note" to a string', () => { + expect( + createRulesBulkSchema.validate>([ + { + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: '# test markdown', + version: 1, + }, + ]).error + ).toBeFalsy(); + }); + + test('You can set "note" to an empty string', () => { + expect( + createRulesBulkSchema.validate>([ + { + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: '', + version: 1, + }, + ]).error + ).toBeFalsy(); + }); + + test('You cannot set "note" to anything other than string', () => { + expect( + createRulesBulkSchema.validate>([ + { + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: { + something: 'some object', + }, + version: 1, + }, + ]).error.message + ).toEqual( + '"value" at position 0 fails because [child "note" fails because ["note" must be a string]]' + ); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 052a149f3d4dc..3bad87dc1a9ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -1224,4 +1224,95 @@ describe('create rules schema', () => { 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' ); }); + + describe('note', () => { + test('You can set note to a string', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: '# documentation markdown here', + }).error + ).toBeFalsy(); + }); + + test('You can set note to an emtpy string', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: '', + }).error + ).toBeFalsy(); + }); + + test('You cannot create note as an object', () => { + expect( + createRulesSchema.validate & { note: object }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: { + somethingHere: 'something else', + }, + }).error.message + ).toEqual('child "note" fails because ["note" must be a string]'); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).error + ).toBeFalsy(); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index eb79e06c8efa6..c9b380d3c67e1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -32,6 +32,7 @@ import { type, threat, references, + note, version, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -67,5 +68,6 @@ export const createRulesSchema = Joi.object({ type: type.required(), threat: threat.default([]), references: references.default([]), + note: note.allow(''), version: version.default(1), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index da441681de50b..9c80ddde9e7b7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -1423,4 +1423,116 @@ describe('import rules schema', () => { 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' ); }); + + describe('note', () => { + test('You can set note to a string', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: false, + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + note: '# test header', + }).error + ).toBeFalsy(); + }); + + test('You can set note to an empty string', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: false, + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + note: '', + }).error + ).toBeFalsy(); + }); + + test('You cannot create note set to null', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: false, + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + note: null, + }).error.message + ).toEqual('child "note" fails because ["note" must be a string]'); + }); + + test('You cannot create note as something other than a string', () => { + expect( + importRulesSchema.validate & { note: object }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: false, + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + note: { + somethingMadeUp: { somethingElse: true }, + }, + }).error.message + ).toEqual('child "note" fails because ["note" must be a string]'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts index 1254694645b9c..bd12872c4dc72 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -38,6 +38,7 @@ import { type, threat, references, + note, version, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -84,6 +85,7 @@ export const importRulesSchema = Joi.object({ type: type.required(), threat: threat.default([]), references: references.default([]), + note: note.allow(''), version: version.default(1), created_at, updated_at, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts index cbcb9eba75bc1..43d1e7ab2aa3b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts @@ -49,4 +49,43 @@ describe('patch_rules_bulk_schema', () => { ]).error ).toBeFalsy(); }); + + test('can set "note" to be a string', () => { + expect( + patchRulesBulkSchema.validate>>([ + { + id: 'rule-1', + note: 'hi', + }, + ]).error + ).toBeFalsy(); + }); + + test('can set "note" to be an empty string', () => { + expect( + patchRulesBulkSchema.validate>>([ + { + id: 'rule-1', + note: '', + }, + ]).error + ).toBeFalsy(); + }); + + test('cannot set "note" to be anything other than a string', () => { + expect( + patchRulesBulkSchema.validate< + Array & { note: object }>> + >([ + { + id: 'rule-1', + note: { + someprop: 'some value here', + }, + }, + ]).error.message + ).toEqual( + '"value" at position 0 fails because [child "note" fails because ["note" must be a string]]' + ); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index 11bed22e1c047..ecdba7ccc0091 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -1012,4 +1012,45 @@ describe('patch rules schema', () => { 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' ); }); + + describe('note', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, note] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + note: '# some documentation markdown', + }).error + ).toBeFalsy(); + }); + + test('note can be patched', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + note: '# new documentation markdown', + }).error + ).toBeFalsy(); + }); + + test('You cannot patch note as an object', () => { + expect( + patchRulesSchema.validate< + Partial & { note: object }> + >({ + id: 'rule-1', + note: { + someProperty: 'something else here', + }, + }).error.message + ).toEqual('child "note" fails because ["note" must be a string]'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts index d0ed1af01833b..4d1b73fb69e5b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -32,6 +32,7 @@ import { type, threat, references, + note, id, version, } from './schemas'; @@ -63,5 +64,6 @@ export const patchRulesSchema = Joi.object({ type, threat, references, + note: note.allow(''), version, }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts index ae2d6269279e1..945b5651be066 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -48,6 +48,7 @@ import { version, filters, meta, + note, } from './schemas'; /** @@ -113,6 +114,7 @@ export const partialRulesSchema = t.partial({ filters, meta, index, + note, }); /** diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index 14de14a8464fb..16f6c0fd6b8b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -128,3 +128,4 @@ export const success_count = PositiveInteger; export const rules_custom_installed = PositiveInteger; export const rules_not_installed = PositiveInteger; export const rules_not_updated = PositiveInteger; +export const note = t.string; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 9b311b1b58ea7..2ba9ec7f83253 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -105,3 +105,4 @@ export const updated_by = Joi.string(); export const version = Joi.number() .integer() .min(1); +export const note = Joi.string(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index c7899f3afa7b8..e37abf3746ae6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -1243,4 +1243,101 @@ describe('create rules schema', () => { 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' ); }); + + describe('note', () => { + test('You can set note to a string', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: '# some documentation title', + }).error + ).toBeFalsy(); + }); + + test('You can set note to an empty string', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: '', + }).error + ).toBeFalsy(); + }); + + // Note: If you're looking to remove `note`, omit `note` entirely + test('You cannot set note to null', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: null, + }).error.message + ).toEqual('child "note" fails because ["note" must be a string]'); + }); + + test('You cannot set note as an object', () => { + expect( + updateRulesSchema.validate & { note: object }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + note: { + somethingMadeUp: { somethingElse: true }, + }, + }).error.message + ).toEqual('child "note" fails because ["note" must be a string]'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index 3e5a608d6b657..a72105142d287 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -33,6 +33,7 @@ import { threat, references, id, + note, version, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -76,5 +77,6 @@ export const updateRulesSchema = Joi.object({ type: type.required(), threat: threat.default([]), references: references.default([]), + note: note.allow(''), version, }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index c8205859407c0..ea87950a59b78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -37,6 +37,7 @@ export const createRules = ({ to, type, references, + note, version, }: CreateRuleParams): Promise => { return alertsClient.create({ @@ -67,6 +68,7 @@ export const createRules = ({ to, type, references, + note, version, }, schedule: { interval }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 33f60bf0ba543..39b596dfed855 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -21,7 +21,7 @@ describe('getExportAll', () => { const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', + '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"note":"# Investigative notes","version":1}\n', exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 83b487163bdfb..1406c7c9000b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -29,7 +29,7 @@ describe('get_export_by_object_ids', () => { const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', + '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"note":"# Investigative notes","version":1}\n', exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', }); }); @@ -117,6 +117,7 @@ describe('get_export_by_object_ids', () => { ], }, ], + note: '# Investigative notes', version: 1, }, ], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 3d9ec128963f6..3b5ef57d3dcb6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -42,6 +42,7 @@ export const installPrepackagedRules = ( type, threat, references, + note, version, } = rule; return [ @@ -74,6 +75,7 @@ export const installPrepackagedRules = ( type, threat, references, + note, version, }), ]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 5fdef59a72f04..628f4033d5665 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -42,6 +42,7 @@ export const patchRules = async ({ to, type, references, + note, version, throttle, }: PatchRuleParams): Promise => { @@ -75,6 +76,7 @@ export const patchRules = async ({ references, version, throttle, + note, }); const nextParams = defaults( @@ -102,6 +104,7 @@ export const patchRules = async ({ to, type, references, + note, version: calculatedVersion, } ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 7889267a7267b..1051ac28885b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -42,6 +42,7 @@ export const updatePrepackagedRules = async ( references, version, throttle, + note, } = rule; // Note: we do not pass down enabled as we do not want to suddenly disable @@ -75,6 +76,7 @@ export const updatePrepackagedRules = async ( references, version, throttle, + note, }); }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 3a10841b70d7e..3987654589bdd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -43,6 +43,7 @@ export const updateRules = async ({ references, version, throttle, + note, }: UpdateRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -74,6 +75,7 @@ export const updateRules = async ({ references, version, throttle, + note, }); const update = await alertsClient.update({ @@ -106,6 +108,7 @@ export const updateRules = async ({ to, type, references, + note, version: calculatedVersion, }, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_note.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_note.json new file mode 100644 index 0000000000000..4262cc63008a1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_note.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-with-note", + "note": " # Changes only the note to this new value" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json index 082dd5205a142..286aa6c771d15 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json @@ -78,5 +78,6 @@ ], "timeline_id": "timeline_id", "timeline_title": "timeline_title", + "note": "# note markdown", "version": 1 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_note.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_note.json new file mode 100644 index 0000000000000..71e6ce2f83040 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_note.json @@ -0,0 +1,10 @@ +{ + "name": "Query with a note", + "description": "Query with a note", + "rule_id": "query-with-note", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "note": "# investigative note markdown header" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_note.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_note.json new file mode 100644 index 0000000000000..de850906b2859 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_note.json @@ -0,0 +1,10 @@ +{ + "name": "Query with a note", + "description": "Query with a note", + "rule_id": "query-with-note", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "note": "# Changes only note to this new value on update" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index fded0696ff8bf..922651edc4082 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -28,6 +28,7 @@ export const sampleRuleAlertParams = ( references: ['http://google.com'], riskScore: riskScore ? riskScore : 50, maxSignals: maxSignals ? maxSignals : 10000, + note: '', filters: undefined, savedId: undefined, timelineId: undefined, @@ -340,6 +341,7 @@ export const sampleRule = (): Partial => { tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', type: 'query', + note: '', }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index b71a7080f4147..30dac114ac506 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -79,6 +79,7 @@ describe('buildBulkBody', () => { tags: ['some fake tag 1', 'some fake tag 2'], type: 'query', to: 'now', + note: '', enabled: true, created_by: 'elastic', updated_by: 'elastic', @@ -168,6 +169,7 @@ describe('buildBulkBody', () => { tags: ['some fake tag 1', 'some fake tag 2'], type: 'query', to: 'now', + note: '', enabled: true, created_by: 'elastic', updated_by: 'elastic', @@ -255,6 +257,7 @@ describe('buildBulkBody', () => { tags: ['some fake tag 1', 'some fake tag 2'], type: 'query', to: 'now', + note: '', enabled: true, created_by: 'elastic', updated_by: 'elastic', @@ -335,6 +338,7 @@ describe('buildBulkBody', () => { tags: ['some fake tag 1', 'some fake tag 2'], type: 'query', to: 'now', + note: '', enabled: true, created_by: 'elastic', updated_by: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index af0883f4ce6b5..c2900782ed676 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -60,6 +60,7 @@ describe('buildRule', () => { tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', type: 'query', + note: '', updated_by: 'elastic', updated_at: rule.updated_at, created_at: rule.created_at, @@ -116,6 +117,7 @@ describe('buildRule', () => { tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', type: 'query', + note: '', updated_by: 'elastic', version: 1, updated_at: rule.updated_at, @@ -161,6 +163,7 @@ describe('buildRule', () => { tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', type: 'query', + note: '', updated_by: 'elastic', version: 1, updated_at: rule.updated_at, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index 70465bf1d9201..9baf6a55b7f48 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -44,6 +44,7 @@ export const buildRule = ({ risk_score: ruleParams.riskScore, output_index: ruleParams.outputIndex, description: ruleParams.description, + note: ruleParams.note, from: ruleParams.from, immutable: ruleParams.immutable, index: ruleParams.index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts index 93e9c7f6e0d50..0a50c33fbbfe4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts @@ -66,6 +66,7 @@ describe('buildSignal', () => { tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', type: 'query', + note: '', updated_at: signal.rule.updated_at, created_at: signal.rule.created_at, }, @@ -131,6 +132,7 @@ describe('buildSignal', () => { tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', type: 'query', + note: '', updated_at: signal.rule.updated_at, created_at: signal.rule.created_at, }, @@ -202,6 +204,7 @@ describe('buildSignal', () => { tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', type: 'query', + note: '', updated_at: signal.rule.updated_at, created_at: signal.rule.created_at, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index f7fabfb980195..b467dfdaff305 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -55,6 +55,7 @@ export const signalRulesAlertType = ({ validate: { params: schema.object({ description: schema.string(), + note: schema.nullable(schema.string()), falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), from: schema.string(), ruleId: schema.string(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index 5e5ff157c92c6..fa43ac1debb92 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -24,6 +24,7 @@ export interface ThreatParams { export interface RuleAlertParams { description: string; + note: string | undefined | null; enabled: boolean; falsePositives: string[]; filters: PartialFilter[] | undefined | null; diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index c3ca0a16df797..d217d26e84836 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -10,8 +10,7 @@ The Kibana actions plugin provides a framework to create executable actions. You - Execute an action, passing it a parameter object. - Perform CRUD operations on actions. ------ - +--- Table of Contents @@ -61,15 +60,18 @@ Table of Contents - [`config`](#config-5) - [`secrets`](#secrets-5) - [`params`](#params-5) + - [ServiceNow](#servicenow) + - [`config`](#config-6) + - [`secrets`](#secrets-6) + - [`params`](#params-6) - [Command Line Utility](#command-line-utility) - ## Terminology -**Action Type**: A programatically defined integration with another service, with an expected set of configuration and parameters properties, typically defined with a schema. Plugins can add new +**Action Type**: A programatically defined integration with another service, with an expected set of configuration and parameters properties, typically defined with a schema. Plugins can add new action types. -**Action**: A configuration object associated with an action type, that is ready to be executed. The configuration is persisted via Saved Objects, and some/none/all of the configuration properties can be stored encrypted. +**Action**: A configuration object associated with an action type, that is ready to be executed. The configuration is persisted via Saved Objects, and some/none/all of the configuration properties can be stored encrypted. ## Usage @@ -78,36 +80,37 @@ action types. 3. Use alerts to execute actions or execute manually (see firing actions). ## Kibana Actions Configuration + Implemented under the [Actions Config](./server/actions_config.ts). ### Configuration Options Built-In-Actions are configured using the _xpack.actions_ namespoace under _kibana.yml_, and have the following configuration options: -| Namespaced Key | Description | Type | -| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | -| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | -| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | +| Namespaced Key | Description | Type | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | +| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | +| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | #### Whitelisting Built-in Action Types + It is worth noting that the **whitelistedHosts** configuation applies to built-in action types (such as Slack, or PagerDuty) as well. Uniquely, the _PagerDuty Action Type_ has been configured to support the service's Events API (at _https://events.pagerduty.com/v2/enqueue_, which you can read about [here](https://v2.developer.pagerduty.com/docs/events-api-v2)) as a default, but this too, must be included in the whitelist before the PagerDuty action can be used. - ### Configuration Utilities This module provides a Utilities for interacting with the configuration. -| Method | Arguments | Description | Return Type | -| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| isWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will always return `true`. | Boolean | -| isWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will always return `true`. | Boolean | -| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | -| ensureWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will never throw. | No return value, throws if URI isn't whitelisted | -| ensureWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will never throw | No return value, throws if Hostname isn't whitelisted | -| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | +| Method | Arguments | Description | Return Type | +| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| isWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will always return `true`. | Boolean | +| isWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will always return `true`. | Boolean | +| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | +| ensureWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will never throw. | No return value, throws if URI isn't whitelisted | +| ensureWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will never throw | No return value, throws if Hostname isn't whitelisted | +| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | ## Action types @@ -117,38 +120,37 @@ This module provides a Utilities for interacting with the configuration. The following table describes the properties of the `options` object. -|Property|Description|Type| -|---|---|---| -|id|Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types.|string| -|name|A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types.|string| -|unencryptedAttributes|A list of opt-out attributes that don't need to be encrypted. These attributes won't need to be re-entered on import / export when the feature becomes available. These attributes will also be readable / displayed when it comes to a table / edit screen.|array of strings| -|validate.params|When developing an action type, it needs to accept parameters to know what to do with the action. (Example to, from, subject, body of an email). See the current built-in email action type for an example of the state-of-the-art validation.

Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message|schema / validation function| -|validate.config|Similar to params, a config is required when creating an action (for example host, port, username, and password of an email server). |schema / validation function| -|executor|This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below.|Function| +| Property | Description | Type | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string | +| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | +| unencryptedAttributes | A list of opt-out attributes that don't need to be encrypted. These attributes won't need to be re-entered on import / export when the feature becomes available. These attributes will also be readable / displayed when it comes to a table / edit screen. | array of strings | +| validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example to, from, subject, body of an email). See the current built-in email action type for an example of the state-of-the-art validation.

Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function | +| validate.config | Similar to params, a config is required when creating an action (for example host, port, username, and password of an email server). | schema / validation function | +| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | -**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. +**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. ### Executor -This is the primary function for an action type. Whenever the action needs to execute, this function will perform the action. It receives a variety of parameters. The following table describes the properties that the executor receives. +This is the primary function for an action type. Whenever the action needs to execute, this function will perform the action. It receives a variety of parameters. The following table describes the properties that the executor receives. **executor(options)** -|Property|Description| -|---|---| -|actionId|The action saved object id that the action type is executing for.| -|config|The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type.| -|params|Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function.| -|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.

**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.| -|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled).| -|services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| +| Property | Description | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| actionId | The action saved object id that the action type is executing for. | +| config | The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | +| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | +| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.

**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR. | +| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | +| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | ### Example -The built-in email action type provides a good example of creating an action type with non-trivial configuration and params: +The built-in email action type provides a good example of creating an action type with non-trivial configuration and params: [x-pack/plugins/actions/server/builtin_action_types/email.ts](server/builtin_action_types/email.ts) - ## RESTful API Using an action type requires an action to be created that will contain and encrypt configuration for a given action type. See below for CRUD operations using the API. @@ -157,20 +159,20 @@ Using an action type requires an action to be created that will contain and encr Payload: -|Property|Description|Type| -|---|---|---| -|name|A name to reference and search in the future. This value will be used to populate dropdowns.|string| -|actionTypeId|The id value of the action type you want to call when the action executes.|string| -|config|The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined.|object| -|secrets|The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined.|object| +| Property | Description | Type | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| name | A name to reference and search in the future. This value will be used to populate dropdowns. | string | +| actionTypeId | The id value of the action type you want to call when the action executes. | string | +| config | The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined. | object | +| secrets | The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined. | object | ### `DELETE /api/action/{id}`: Delete action Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to delete.|string| +| Property | Description | Type | +| -------- | --------------------------------------------- | ------ | +| id | The id of the action you're trying to delete. | string | ### `GET /api/action/_find`: Find actions @@ -182,9 +184,9 @@ See the [saved objects API documentation for find](https://www.elastic.co/guide/ Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to get.|string| +| Property | Description | Type | +| -------- | ------------------------------------------ | ------ | +| id | The id of the action you're trying to get. | string | ### `GET /api/action/types`: List action types @@ -194,31 +196,31 @@ No parameters. Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to update.|string| +| Property | Description | Type | +| -------- | --------------------------------------------- | ------ | +| id | The id of the action you're trying to update. | string | Payload: -|Property|Description|Type| -|---|---|---| -|name|A name to reference and search in the future. This value will be used to populate dropdowns.|string| -|config|The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined.|object| -|secrets|The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined.|object| +| Property | Description | Type | +| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| name | A name to reference and search in the future. This value will be used to populate dropdowns. | string | +| config | The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined. | object | +| secrets | The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined. | object | ### `POST /api/action/{id}/_execute`: Execute action Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to execute.|string| +| Property | Description | Type | +| -------- | ---------------------------------------------- | ------ | +| id | The id of the action you're trying to execute. | string | Payload: -|Property|Description|Type| -|---|---|---| -|params|The parameters the action type requires for the execution.|object| +| Property | Description | Type | +| -------- | ---------------------------------------------------------- | ------ | +| params | The parameters the action type requires for the execution. | object | ## Firing actions @@ -228,12 +230,12 @@ The plugin exposes an execute function that you can use to run actions. The following table describes the properties of the `options` object. -|Property|Description|Type| -|---|---|---| -|id|The id of the action you want to execute.|string| -|params|The `params` value to give the action type executor.|object| -|spaceId|The space id the action is within.|string| -|apiKey|The Elasticsearch API key to use for context. (Note: only required and used when security is enabled).|string| +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------ | ------ | +| id | The id of the action you want to execute. | string | +| params | The `params` value to give the action type executor. | object | +| spaceId | The space id the action is within. | string | +| apiKey | The Elasticsearch API key to use for context. (Note: only required and used when security is enabled). | string | ## Example @@ -256,23 +258,25 @@ server.plugins.actions.execute({ Kibana ships with a set of built-in action types: -|Type|Id|Description| -|---|---|---| -|[Server log](#server-log)|`.log`|Logs messages to the Kibana log using `server.log()`| -|[Email](#email)|`.email`|Sends an email using SMTP| -|[Slack](#slack)|`.slack`|Posts a message to a slack channel| -|[Index](#index)|`.index`|Indexes document(s) into Elasticsearch| -|[Webhook](#webhook)|`.webhook`|Send a payload to a web service using HTTP POST or PUT| -|[PagerDuty](#pagerduty)|`.pagerduty`|Trigger, resolve, or acknowlege an incident to a PagerDuty service| +| Type | Id | Description | +| ------------------------- | ------------- | ------------------------------------------------------------------ | +| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` | +| [Email](#email) | `.email` | Sends an email using SMTP | +| [Slack](#slack) | `.slack` | Posts a message to a slack channel | +| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | +| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT | +| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service | +| [ServiceNow](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow instance | + +--- ----- ## Server log ID: `.log` The params properties are modelled after the arguments to the [Hapi.server.log()](https://hapijs.com/api#-serverlogtags-data-timestamp) function. -### `config` +### `config` This action has no `config` properties. @@ -282,12 +286,13 @@ This action type has no `secrets` properties. ### `params` -|Property|Description|Type| -|---|---|---| -|message|The message to log.|string| -|tags|Tags associated with the message to log.|string[] _(optional)_| +| Property | Description | Type | +| -------- | ---------------------------------------- | --------------------- | +| message | The message to log. | string | +| tags | Tags associated with the message to log. | string[] _(optional)_ | + +--- ----- ## Email ID: `.email` @@ -296,50 +301,50 @@ This action type uses [nodemailer](https://nodemailer.com/about/) to send emails ### `config` -Either the property `service` must be provided, or the `host` and `port` properties must be provided. If `service` is provided, `host`, `port` and `secure` are ignored. For more information on the `gmail` service value specifically, see the [nodemailer gmail documentation](https://nodemailer.com/usage/using-gmail/). +Either the property `service` must be provided, or the `host` and `port` properties must be provided. If `service` is provided, `host`, `port` and `secure` are ignored. For more information on the `gmail` service value specifically, see the [nodemailer gmail documentation](https://nodemailer.com/usage/using-gmail/). -The `secure` property defaults to `false`. See the [nodemailer TLS documentation](https://nodemailer.com/smtp/#tls-options) for more information. +The `secure` property defaults to `false`. See the [nodemailer TLS documentation](https://nodemailer.com/smtp/#tls-options) for more information. -The `from` field can be specified as in typical `"user@host-name"` format, or as `"human name "` format. See the [nodemailer address documentation](https://nodemailer.com/message/addresses/) for more information. +The `from` field can be specified as in typical `"user@host-name"` format, or as `"human name "` format. See the [nodemailer address documentation](https://nodemailer.com/message/addresses/) for more information. -|Property|Description|Type| -|---|---|---| -|service|the name of a [well-known email service provider](https://nodemailer.com/smtp/well-known/)|string _(optional)_| -|host|host name of the service provider|string _(optional)_| -|port|port number of the service provider|number _(optional)_| -|secure|whether to use TLS with the service provider|boolean _(optional)_| -|from|the from address for all emails sent with this action type|string| +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------ | -------------------- | +| service | the name of a [well-known email service provider](https://nodemailer.com/smtp/well-known/) | string _(optional)_ | +| host | host name of the service provider | string _(optional)_ | +| port | port number of the service provider | number _(optional)_ | +| secure | whether to use TLS with the service provider | boolean _(optional)_ | +| from | the from address for all emails sent with this action type | string | ### `secrets` -|Property|Description|Type| -|---|---|---| -|user|userid to use with the service provider|string| -|password|password to use with the service provider|string| +| Property | Description | Type | +| -------- | ----------------------------------------- | ------ | +| user | userid to use with the service provider | string | +| password | password to use with the service provider | string | ### `params` There must be at least one entry in the `to`, `cc` and `bcc` arrays. -The message text will be sent as both plain text and html text. Additional function may be provided later. +The message text will be sent as both plain text and html text. Additional function may be provided later. The `to`, `cc`, and `bcc` array entries can be in the same format as the `from` property described in the config object above. -|Property|Description|Type| -|---|---|---| -|to|list of to addressees|string[] _(optional)_| -|cc|list of cc addressees|string[] _(optional)_| -|bcc|list of bcc addressees|string[] _(optional)_| -|subject|the subject line of the email|string| -|message|the message text|string| +| Property | Description | Type | +| -------- | ----------------------------- | --------------------- | +| to | list of to addressees | string[] _(optional)_ | +| cc | list of cc addressees | string[] _(optional)_ | +| bcc | list of bcc addressees | string[] _(optional)_ | +| subject | the subject line of the email | string | +| message | the message text | string | ----- +--- ## Slack ID: `.slack` -This action type interfaces with the [Slack Incoming Webhooks feature](https://api.slack.com/incoming-webhooks). Currently the params property `message` will be used as the `text` property of the Slack incoming message. Additional function may be provided later. +This action type interfaces with the [Slack Incoming Webhooks feature](https://api.slack.com/incoming-webhooks). Currently the params property `message` will be used as the `text` property of the Slack incoming message. Additional function may be provided later. ### `config` @@ -347,29 +352,29 @@ This action type has no `config` properties. ### `secrets` -|Property|Description|Type| -|---|---|---| -|webhookUrl|the url of the Slack incoming webhook|string| +| Property | Description | Type | +| ---------- | ------------------------------------- | ------ | +| webhookUrl | the url of the Slack incoming webhook | string | ### `params` -|Property|Description|Type| -|---|---|---| -|message|the message text|string| +| Property | Description | Type | +| -------- | ---------------- | ------ | +| message | the message text | string | ----- +--- ## Index ID: `.index` -The config and params properties are modelled after the [Watcher Index Action](https://www.elastic.co/guide/en/elasticsearch/reference/master/actions-index.html). The index can be set in the config or params, and if set in config, then the index set in the params will be ignored. +The config and params properties are modelled after the [Watcher Index Action](https://www.elastic.co/guide/en/elasticsearch/reference/master/actions-index.html). The index can be set in the config or params, and if set in config, then the index set in the params will be ignored. ### `config` -|Property|Description|Type| -|---|---|---| -|index|The Elasticsearch index to index into.|string _(optional)_| +| Property | Description | Type | +| -------- | -------------------------------------- | ------------------- | +| index | The Elasticsearch index to index into. | string _(optional)_ | ### `secrets` @@ -377,81 +382,114 @@ This action type has no `secrets` properties. ### `params` -|Property|Description|Type| -|---|---|---| -|index|The Elasticsearch index to index into.|string _(optional)_| -|doc_id|The optional _id of the document.|string _(optional)_| -|execution_time_field|The field that will store/index the action execution time.|string _(optional)_| -|refresh|Setting of the refresh policy for the write request|boolean _(optional)_| -|body|The documument body/bodies to index.|object or object[]| +| Property | Description | Type | +| -------------------- | ---------------------------------------------------------- | -------------------- | +| index | The Elasticsearch index to index into. | string _(optional)_ | +| doc_id | The optional \_id of the document. | string _(optional)_ | +| execution_time_field | The field that will store/index the action execution time. | string _(optional)_ | +| refresh | Setting of the refresh policy for the write request | boolean _(optional)_ | +| body | The documument body/bodies to index. | object or object[] | + +--- ----- ## Webhook ID: `.webhook` The webhook action uses [axios](https://github.com/axios/axios) to send a POST or PUT request to a web service. -### `config` +### `config` -|Property|Description|Type| -|---|---|---| -|url|Request URL|string| -|method|HTTP request method, either `post`_(default)_ or `put`|string _(optional)_| -|headers|Key-value pairs of the headers to send with the request|object, keys and values are strings _(optional)_| +| Property | Description | Type | +| -------- | ------------------------------------------------------- | ------------------------------------------------ | +| url | Request URL | string | +| method | HTTP request method, either `post`_(default)_ or `put` | string _(optional)_ | +| headers | Key-value pairs of the headers to send with the request | object, keys and values are strings _(optional)_ | -### `secrets` +### `secrets` -|Property|Description|Type| -|---|---|---| -|user|Username for HTTP Basic authentication|string _(optional)_| -|password|Password for HTTP Basic authentication|string _(optional)_| +| Property | Description | Type | +| -------- | -------------------------------------- | ------------------- | +| user | Username for HTTP Basic authentication | string _(optional)_ | +| password | Password for HTTP Basic authentication | string _(optional)_ | -### `params` +### `params` -|Property|Description|Type| -|---|---|---| -|body|The HTTP request body|string _(optional)_| +| Property | Description | Type | +| -------- | --------------------- | ------------------- | +| body | The HTTP request body | string _(optional)_ | ----- +--- ## PagerDuty -ID: `.pagerduty` +ID: `.pagerduty` -The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com/docs/events-api-v2) to trigger, acknowlege, and resolve PagerDuty alerts. +The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com/docs/events-api-v2) to trigger, acknowlege, and resolve PagerDuty alerts. -### `config` +### `config` -|Property|Description|Type| -|---|---|---| -|apiUrl|PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`|string _(optional)_| +| Property | Description | Type | +| -------- | -------------------------------------------------------------------------- | ------------------- | +| apiUrl | PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue` | string _(optional)_ | ### `secrets` -|Property|Description|Type| -|---|---|---| -|routingKey|This is the 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset.|string| +| Property | Description | Type | +| ---------- | ---------------------------------------------------------------------------------------------------------- | ------ | +| routingKey | This is the 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. | string | -### `params` +### `params` -|Property|Description|Type| -|---|---|---| -|eventAction|One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details.| string _(optional)_| -|dedupKey|All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. Defaults to `action:`. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_| -|summary|A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_| -|source|The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `.| string _(optional)_| -|severity|The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_.| string _(optional)_| -|timestamp|An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated.| string _(optional)_| -|component|The component of the source machine that is responsible for the event, for example `mysql` or `eth0`.| string _(optional)_| -|group|Logical grouping of components of a service, for example `app-stack`.| string _(optional)_| -|class|The class/type of the event, for example `ping failure` or `cpu load`.| string _(optional)_| +| Property | Description | Type | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ | +| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. Defaults to `action:`. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | +| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ | +| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ | +| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ | +| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ | +| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ | +| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ | +| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ | For more details see [PagerDuty v2 event parameters](https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2). +--- + +## ServiceNow + +ID: `.servicenow` + +The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/app.do#!/rest_api_doc?v=orlando&id=c_TableAPI) to create and update ServiceNow incidents. + +### `config` + +| Property | Description | Type | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| apiUrl | ServiceNow instance URL. | string | +| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object | + +### `secrets` + +| Property | Description | Type | +| -------- | -------------------------------------- | ------ | +| username | Username for HTTP Basic authentication | string | +| password | Password for HTTP Basic authentication | string | + +### `params` + +| Property | Description | Type | +| ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| caseId | The case id | string | +| title | The title of the case | string _(optional)_ | +| description | The description of the case | string _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| incidentID | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | + # Command Line Utility -The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: +The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: ```console $ kbn-action create .slack "post to slack" '{"webhookUrl": "https://hooks.slack.com/services/T0000/B0000/XXXX"}' @@ -467,4 +505,4 @@ $ kbn-action create .slack "post to slack" '{"webhookUrl": "https://hooks.slack. "updated_at": "2019-06-26T17:55:42.728Z", "version": "WzMsMV0=" } -``` \ No newline at end of file +``` diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts deleted file mode 100644 index cfd3a9d70dc93..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios, { AxiosResponse } from 'axios'; -import { Services } from '../../types'; -import { ParamsType, SecretsType } from '../servicenow'; - -interface PostServiceNowOptions { - apiUrl: string; - data: ParamsType; - headers: Record; - services?: Services; - secrets: SecretsType; -} - -// post an event to serviceNow -export async function postServiceNow(options: PostServiceNowOptions): Promise { - const { apiUrl, data, headers, secrets } = options; - const axiosOptions = { - headers, - validateStatus: () => true, - auth: secrets, - }; - return axios.post(`${apiUrl}/api/now/v1/table/incident`, data, axiosOptions); -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts deleted file mode 100644 index 9ae96cb23a5c3..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('./lib/post_servicenow', () => ({ - postServiceNow: jest.fn(), -})); - -import { getActionType } from './servicenow'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; -import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; -import { postServiceNow } from './lib/post_servicenow'; -import { createActionTypeRegistry } from './index.test'; -import { configUtilsMock } from '../actions_config.mock'; - -const postServiceNowMock = postServiceNow as jest.Mock; - -const ACTION_TYPE_ID = '.servicenow'; - -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; - -let actionType: ActionType; - -const mockServiceNow = { - config: { - apiUrl: 'www.servicenowisinkibanaactions.com', - }, - secrets: { - password: 'secret-password', - username: 'secret-username', - }, - params: { - comments: 'hello cool service now incident', - short_description: 'this is a cool service now incident', - }, -}; - -beforeAll(() => { - const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); -}); - -describe('get()', () => { - test('should return correct action type', () => { - expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual('ServiceNow'); - }); -}); - -describe('validateConfig()', () => { - test('should validate and pass when config is valid', () => { - const { config } = mockServiceNow; - expect(validateConfig(actionType, config)).toEqual(config); - }); - - test('should validate and throw error when config is invalid', () => { - expect(() => { - validateConfig(actionType, { shouldNotBeHere: true }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"` - ); - }); - - test('should validate and pass when the servicenow url is whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...configUtilsMock, - ensureWhitelistedUri: url => { - expect(url).toEqual('https://events.servicenow.com/v2/enqueue'); - }, - }, - }); - - expect( - validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' }) - ).toEqual({ apiUrl: 'https://events.servicenow.com/v2/enqueue' }); - }); - - test('config validation returns an error if the specified URL isnt whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...configUtilsMock, - ensureWhitelistedUri: _ => { - throw new Error(`target url is not whitelisted`); - }, - }, - }); - - expect(() => { - validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` - ); - }); -}); - -describe('validateSecrets()', () => { - test('should validate and pass when secrets is valid', () => { - const { secrets } = mockServiceNow; - expect(validateSecrets(actionType, secrets)).toEqual(secrets); - }); - - test('should validate and throw error when secrets is invalid', () => { - expect(() => { - validateSecrets(actionType, { username: false }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"` - ); - - expect(() => { - validateSecrets(actionType, { username: false, password: 'hello' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"` - ); - }); -}); - -describe('validateParams()', () => { - test('should validate and pass when params is valid', () => { - const { params } = mockServiceNow; - expect(validateParams(actionType, params)).toEqual(params); - }); - - test('should validate and throw error when params is invalid', () => { - expect(() => { - validateParams(actionType, { eventAction: 'ackynollage' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [short_description]: expected value of type [string] but got [undefined]"` - ); - }); -}); - -describe('execute()', () => { - beforeEach(() => { - postServiceNowMock.mockReset(); - }); - const { config, params, secrets } = mockServiceNow; - test('should succeed with valid params', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 201, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - const { apiUrl, data, headers } = postServiceNowMock.mock.calls[0][0]; - expect({ apiUrl, data, headers, secrets }).toMatchInlineSnapshot(` - Object { - "apiUrl": "www.servicenowisinkibanaactions.com", - "data": Object { - "comments": "hello cool service now incident", - "short_description": "this is a cool service now incident", - }, - "headers": Object { - "Accept": "application/json", - "Content-Type": "application/json", - }, - "secrets": Object { - "password": "secret-password", - "username": "secret-username", - }, - } - `); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "data": "data-here", - "status": "ok", - } - `); - }); - - test('should fail when postServiceNow throws', async () => { - postServiceNowMock.mockImplementation(() => { - throw new Error('doing some testing'); - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event", - "serviceMessage": "doing some testing", - "status": "error", - } - `); - }); - - test('should fail when postServiceNow returns 429', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 429, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: http status 429, retry later", - "retry": true, - "status": "error", - } - `); - }); - - test('should fail when postServiceNow returns 501', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 501, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: http status 501, retry later", - "retry": true, - "status": "error", - } - `); - }); - - test('should fail when postServiceNow returns 418', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 418, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: unexpected status 418", - "status": "error", - } - `); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts deleted file mode 100644 index 0ad435281eba4..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { curry } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { schema, TypeOf } from '@kbn/config-schema'; -import { - ActionType, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, - ExecutorType, -} from '../types'; -import { ActionsConfigurationUtilities } from '../actions_config'; -import { postServiceNow } from './lib/post_servicenow'; - -// config definition -export type ConfigType = TypeOf; - -const ConfigSchemaProps = { - apiUrl: schema.string(), -}; - -const ConfigSchema = schema.object(ConfigSchemaProps); - -function validateConfig( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ConfigType -) { - if (configObject.apiUrl == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiNullError', { - defaultMessage: 'ServiceNow [apiUrl] is required', - }); - } - try { - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { - defaultMessage: 'error configuring servicenow action: {message}', - values: { - message: whitelistError.message, - }, - }); - } -} -// secrets definition -export type SecretsType = TypeOf; -const SecretsSchemaProps = { - password: schema.string(), - username: schema.string(), -}; - -const SecretsSchema = schema.object(SecretsSchemaProps); - -function validateSecrets( - configurationUtilities: ActionsConfigurationUtilities, - secrets: SecretsType -) { - if (secrets.username == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiUserError', { - defaultMessage: 'error configuring servicenow action: no secrets [username] provided', - }); - } - if (secrets.password == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiPasswordError', { - defaultMessage: 'error configuring servicenow action: no secrets [password] provided', - }); - } -} - -// params definition - -export type ParamsType = TypeOf; - -const ParamsSchema = schema.object({ - comments: schema.maybe(schema.string()), - short_description: schema.string(), -}); - -// action type definition -export function getActionType({ - configurationUtilities, - executor = serviceNowExecutor, -}: { - configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { - return { - id: '.servicenow', - name: i18n.translate('xpack.actions.builtin.servicenowTitle', { - defaultMessage: 'ServiceNow', - }), - validate: { - config: schema.object(ConfigSchemaProps, { - validate: curry(validateConfig)(configurationUtilities), - }), - secrets: schema.object(SecretsSchemaProps, { - validate: curry(validateSecrets)(configurationUtilities), - }), - params: ParamsSchema, - }, - executor, - }; -} - -// action executor - -async function serviceNowExecutor( - execOptions: ActionTypeExecutorOptions -): Promise { - const actionId = execOptions.actionId; - const config = execOptions.config as ConfigType; - const secrets = execOptions.secrets as SecretsType; - const params = execOptions.params as ParamsType; - const headers = { - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - let response; - try { - response = await postServiceNow({ apiUrl: config.apiUrl, data: params, headers, secrets }); - } catch (err) { - const message = i18n.translate('xpack.actions.builtin.servicenow.postingErrorMessage', { - defaultMessage: 'error posting servicenow event', - }); - return { - status: 'error', - actionId, - message, - serviceMessage: err.message, - }; - } - if (response.status === 200 || response.status === 201 || response.status === 204) { - return { - status: 'ok', - actionId, - data: response.data, - }; - } - - if (response.status === 429 || response.status >= 500) { - const message = i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { - defaultMessage: 'error posting servicenow event: http status {status}, retry later', - values: { - status: response.status, - }, - }); - - return { - status: 'error', - actionId, - message, - retry: true, - }; - } - - const message = i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { - defaultMessage: 'error posting servicenow event: unexpected status {status}', - values: { - status: response.status, - }, - }); - - return { - status: 'error', - actionId, - message, - }; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts new file mode 100644 index 0000000000000..381b44439033c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { ServiceNow } from './lib'; +import { finalMapping } from './mock'; +import { Incident } from './lib/types'; + +jest.mock('./lib'); + +const ServiceNowMock = ServiceNow as jest.Mock; + +const incident: Incident = { + short_description: 'A title', + description: 'A description', +}; + +const comments = [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: undefined, + }, +]; + +describe('handleCreateIncident', () => { + beforeAll(() => { + ServiceNowMock.mockImplementation(() => { + return { + serviceNow: { + getUserID: jest.fn().mockResolvedValue('1234'), + createIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + updateIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + batchCreateComments: jest + .fn() + .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), + batchUpdateComments: jest + .fn() + .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), + }, + }; + }); + }); + + test('create an incident without comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleCreateIncident({ + serviceNow, + params: incident, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.createIncident).toHaveBeenCalled(); + expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('create an incident with comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleCreateIncident({ + serviceNow, + params: incident, + comments, + mapping: finalMapping, + }); + + expect(serviceNow.createIncident).toHaveBeenCalled(); + expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('update an incident without comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params: incident, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('update an incident and create new comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params: incident, + comments, + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); + + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts new file mode 100644 index 0000000000000..47120c5da096d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { zipWith } from 'lodash'; +import { Incident, CommentResponse } from './lib/types'; +import { + ActionHandlerArguments, + UpdateParamsType, + UpdateActionHandlerArguments, + IncidentCreationResponse, + CommentType, + CommentsZipped, +} from './types'; +import { ServiceNow } from './lib'; + +const createComments = async ( + serviceNow: ServiceNow, + incidentId: string, + key: string, + comments: CommentType[] +): Promise => { + const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key); + + return zipWith(comments, createdComments, (a: CommentType, b: CommentResponse) => ({ + commentId: a.commentId, + pushedDate: b.pushedDate, + })); +}; + +export const handleCreateIncident = async ({ + serviceNow, + params, + comments, + mapping, +}: ActionHandlerArguments): Promise => { + const paramsAsIncident = params as Incident; + + const { incidentId, number, pushedDate } = await serviceNow.createIncident({ + ...paramsAsIncident, + }); + + const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + + if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = [ + ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ]; + } + + return { ...res }; +}; + +export const handleUpdateIncident = async ({ + incidentId, + serviceNow, + params, + comments, + mapping, +}: UpdateActionHandlerArguments): Promise => { + const paramsAsIncident = params as UpdateParamsType; + + const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { + ...paramsAsIncident, + }); + + const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + + if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = [ + ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ]; + } + + return { ...res }; +}; diff --git a/x-pack/legacy/plugins/maps/public/inspector/views/register_views.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts similarity index 62% rename from x-pack/legacy/plugins/maps/public/inspector/views/register_views.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts index 59c0595668300..a0ffd859e14ca 100644 --- a/x-pack/legacy/plugins/maps/public/inspector/views/register_views.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts @@ -4,9 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { npSetup } from 'ui/new_platform'; - -// @ts-ignore -import { MapView } from './map_view'; - -npSetup.plugins.inspector.registerView(MapView); +export const ACTION_TYPE_ID = '.servicenow'; +export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts new file mode 100644 index 0000000000000..96962b41b3c68 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { normalizeMapping, buildMap, mapParams } from './helpers'; +import { mapping, finalMapping } from './mock'; +import { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { MapsType } from './types'; + +const maliciousMapping: MapsType[] = [ + { source: '__proto__', target: 'short_description', actionType: 'nothing' }, + { source: 'description', target: '__proto__', actionType: 'nothing' }, + { source: 'comments', target: 'comments', actionType: 'nothing' }, + { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, +]; + +describe('sanitizeMapping', () => { + test('remove malicious fields', () => { + const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( + true + ); + }); + + test('remove unsuppported source fields', () => { + const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(normalizedMapping).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + source: 'unsupportedSource', + target: 'comments', + actionType: 'nothing', + }), + ]) + ); + }); +}); + +describe('buildMap', () => { + test('builds sanitized Map', () => { + const finalMap = buildMap(maliciousMapping); + expect(finalMap.get('__proto__')).not.toBeDefined(); + }); + + test('builds Map correct', () => { + const final = buildMap(mapping); + expect(final).toEqual(finalMapping); + }); +}); + +describe('mapParams', () => { + test('maps params correctly', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + + const fields = mapParams(params, finalMapping); + + expect(fields).toEqual({ + short_description: 'Incident title', + description: 'Incident description', + }); + }); + + test('do not add fields not in mapping', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + const fields = mapParams(params, finalMapping); + + const { title, description, ...unexpectedFields } = params; + + expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts new file mode 100644 index 0000000000000..99e67c1c43f35 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { MapsType, FinalMapping } from './types'; + +export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { + // Prevent prototype pollution and remove unsupported fields + return mapping.filter( + m => m.source !== '__proto__' && m.target !== '__proto__' && fields.includes(m.source) + ); +}; + +export const buildMap = (mapping: MapsType[]): FinalMapping => { + return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { + const { source, target, actionType } = field; + fieldsMap.set(source, { target, actionType }); + fieldsMap.set(target, { target: source, actionType }); + return fieldsMap; + }, new Map()); +}; + +interface KeyAny { + [key: string]: unknown; +} + +export const mapParams = (params: any, mapping: FinalMapping) => { + return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { + const field = mapping.get(curr); + if (field) { + prev[field.target] = params[curr]; + } + return prev; + }, {}); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts new file mode 100644 index 0000000000000..a1df243b0ee7c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getActionType } from '.'; +import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; +import { validateConfig, validateSecrets, validateParams } from '../../lib'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { createActionTypeRegistry } from '../index.test'; +import { configUtilsMock } from '../../actions_config.mock'; + +import { ACTION_TYPE_ID } from './constants'; +import * as i18n from './translations'; + +import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { incidentResponse } from './mock'; + +jest.mock('./action_handlers'); + +const handleCreateIncidentMock = handleCreateIncident as jest.Mock; +const handleUpdateIncidentMock = handleUpdateIncident as jest.Mock; + +const services: Services = { + callCluster: async (path: string, opts: any) => {}, + savedObjectsClient: savedObjectsClientMock.create(), +}; + +let actionType: ActionType; + +const mockOptions = { + name: 'servicenow-connector', + actionTypeId: '.servicenow', + secrets: { + username: 'secret-username', + password: 'secret-password', + }, + config: { + apiUrl: 'https://service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'work_notes', + actionType: 'append', + }, + ], + }, + }, + params: { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + }, + ], + }, +}; + +beforeAll(() => { + const { actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get(ACTION_TYPE_ID); +}); + +describe('get()', () => { + test('should return correct action type', () => { + expect(actionType.id).toEqual(ACTION_TYPE_ID); + expect(actionType.name).toEqual(i18n.NAME); + }); +}); + +describe('validateConfig()', () => { + test('should validate and pass when config is valid', () => { + const { config } = mockOptions; + expect(validateConfig(actionType, config)).toEqual(config); + }); + + test('should validate and throw error when config is invalid', () => { + expect(() => { + validateConfig(actionType, { shouldNotBeHere: true }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"` + ); + }); + + test('should validate and pass when the servicenow url is whitelisted', () => { + actionType = getActionType({ + configurationUtilities: { + ...configUtilsMock, + ensureWhitelistedUri: url => { + expect(url).toEqual(mockOptions.config.apiUrl); + }, + }, + }); + + expect(validateConfig(actionType, mockOptions.config)).toEqual(mockOptions.config); + }); + + test('config validation returns an error if the specified URL isnt whitelisted', () => { + actionType = getActionType({ + configurationUtilities: { + ...configUtilsMock, + ensureWhitelistedUri: _ => { + throw new Error(`target url is not whitelisted`); + }, + }, + }); + + expect(() => { + validateConfig(actionType, mockOptions.config); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` + ); + }); +}); + +describe('validateSecrets()', () => { + test('should validate and pass when secrets is valid', () => { + const { secrets } = mockOptions; + expect(validateSecrets(actionType, secrets)).toEqual(secrets); + }); + + test('should validate and throw error when secrets is invalid', () => { + expect(() => { + validateSecrets(actionType, { username: false }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"` + ); + + expect(() => { + validateSecrets(actionType, { username: false, password: 'hello' }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"` + ); + }); +}); + +describe('validateParams()', () => { + test('should validate and pass when params is valid', () => { + const { params } = mockOptions; + expect(validateParams(actionType, params)).toEqual(params); + }); + + test('should validate and throw error when params is invalid', () => { + expect(() => { + validateParams(actionType, {}); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [caseId]: expected value of type [string] but got [undefined]"` + ); + }); +}); + +describe('execute()', () => { + beforeEach(() => { + handleCreateIncidentMock.mockReset(); + handleUpdateIncidentMock.mockReset(); + }); + + test('should create an incident', async () => { + const actionId = 'some-id'; + const { incidentId, ...rest } = mockOptions.params; + + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...rest }, + secrets: mockOptions.secrets, + services, + }; + + handleCreateIncidentMock.mockImplementation(() => incidentResponse); + + const actionResponse = await actionType.executor(executorOptions); + expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); + }); + + test('should throw an error when failed to create incident', async () => { + expect.assertions(1); + const { incidentId, ...rest } = mockOptions.params; + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...rest }, + secrets: mockOptions.secrets, + services, + }; + const errorMessage = 'Failed to create incident'; + + handleCreateIncidentMock.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } + }); + + test('should update an incident', async () => { + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'updateIncident' }, + secrets: mockOptions.secrets, + services, + }; + + const actionResponse = await actionType.executor(executorOptions); + expect(actionResponse).toEqual({ actionId, status: 'ok' }); + }); + + test('should throw an error when failed to update an incident', async () => { + expect.assertions(1); + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'updateIncident' }, + secrets: mockOptions.secrets, + services, + }; + const errorMessage = 'Failed to update incident'; + + handleUpdateIncidentMock.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts new file mode 100644 index 0000000000000..01e566af17d08 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { curry, isEmpty } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { + ActionType, + ActionTypeExecutorOptions, + ActionTypeExecutorResult, + ExecutorType, +} from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ServiceNow } from './lib'; + +import * as i18n from './translations'; + +import { ACTION_TYPE_ID } from './constants'; +import { ConfigType, SecretsType, ParamsType, CommentType } from './types'; + +import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; + +import { buildMap, mapParams } from './helpers'; +import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; + +function validateConfig( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ConfigType +) { + try { + if (isEmpty(configObject.casesConfiguration.mapping)) { + return i18n.MAPPING_EMPTY; + } + + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +} + +function validateSecrets( + configurationUtilities: ActionsConfigurationUtilities, + secrets: SecretsType +) {} + +// action type definition +export function getActionType({ + configurationUtilities, + executor = serviceNowExecutor, +}: { + configurationUtilities: ActionsConfigurationUtilities; + executor?: ExecutorType; +}): ActionType { + return { + id: ACTION_TYPE_ID, + name: i18n.NAME, + validate: { + config: schema.object(ConfigSchemaProps, { + validate: curry(validateConfig)(configurationUtilities), + }), + secrets: schema.object(SecretsSchemaProps, { + validate: curry(validateSecrets)(configurationUtilities), + }), + params: ParamsSchema, + }, + executor, + }; +} + +// action executor + +async function serviceNowExecutor( + execOptions: ActionTypeExecutorOptions +): Promise { + const actionId = execOptions.actionId; + const { + apiUrl, + casesConfiguration: { mapping }, + } = execOptions.config as ConfigType; + const { username, password } = execOptions.secrets as SecretsType; + const params = execOptions.params as ParamsType; + const { comments, incidentId, ...restParams } = params; + + const finalMap = buildMap(mapping); + const restParamsMapped = mapParams(restParams, finalMap); + const serviceNow = new ServiceNow({ url: apiUrl, username, password }); + + const handlerInput = { + serviceNow, + params: restParamsMapped, + comments: comments as CommentType[], + mapping: finalMap, + }; + + const res: Pick & + Pick = { + status: 'ok', + actionId, + }; + + let data = {}; + + if (!incidentId) { + data = await handleCreateIncident(handlerInput); + } else { + data = await handleUpdateIncident({ incidentId, ...handlerInput }); + } + + return { + ...res, + data, + }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts new file mode 100644 index 0000000000000..c84e1928e2e5a --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const API_VERSION = 'v2'; +export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; +export const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts new file mode 100644 index 0000000000000..22be625611e85 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import axios from 'axios'; +import { ServiceNow } from '.'; +import { instance, params } from '../mock'; + +jest.mock('axios'); + +axios.create = jest.fn(() => axios); +const axiosMock = (axios as unknown) as jest.Mock; + +let serviceNow: ServiceNow; + +const testMissingConfiguration = (field: string) => { + expect.assertions(1); + try { + new ServiceNow({ ...instance, [field]: '' }); + } catch (error) { + expect(error.message).toEqual('[Action][ServiceNow]: Wrong configuration.'); + } +}; + +const prependInstanceUrl = (url: string): string => `${instance.url}/${url}`; + +describe('ServiceNow lib', () => { + beforeEach(() => { + serviceNow = new ServiceNow(instance); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should thrown an error if url is missing', () => { + testMissingConfiguration('url'); + }); + + test('should thrown an error if username is missing', () => { + testMissingConfiguration('username'); + }); + + test('should thrown an error if password is missing', () => { + testMissingConfiguration('password'); + }); + + test('get user id', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: [{ sys_id: '123' }] }, + }); + + const res = await serviceNow.getUserID(); + const [url, { method }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl('api/now/v2/table/sys_user?user_name=username')); + expect(method).toEqual('get'); + expect(res).toEqual('123'); + }); + + test('create incident', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + }); + + const res = await serviceNow.createIncident({ + short_description: 'A title', + description: 'A description', + caller_id: '123', + }); + const [url, { method, data }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident')); + expect(method).toEqual('post'); + expect(data).toEqual({ + short_description: 'A title', + description: 'A description', + caller_id: '123', + }); + + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('update incident', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + }); + + const res = await serviceNow.updateIncident('123', { + short_description: params.title, + }); + const [url, { method, data }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); + expect(method).toEqual('patch'); + expect(data).toEqual({ short_description: params.title }); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('create comment', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, + }); + + const comment = { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: undefined, + }; + + const res = await serviceNow.createComment('123', comment, 'comments'); + + const [url, { method, data }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); + expect(method).toEqual('patch'); + expect(data).toEqual({ + comments: 'A comment', + }); + + expect(res).toEqual({ + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('create batch comment', async () => { + axiosMock.mockReturnValueOnce({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, + }); + + axiosMock.mockReturnValueOnce({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_updated_on: '2020-03-10 12:25:20' } }, + }); + + const comments = [ + { + commentId: '123', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: undefined, + }, + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A second comment', + incidentCommentId: undefined, + }, + ]; + const res = await serviceNow.batchCreateComments('000', comments, 'comments'); + + comments.forEach((comment, index) => { + const [url, { method, data }] = axiosMock.mock.calls[index]; + expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident/000')); + expect(method).toEqual('patch'); + expect(data).toEqual({ + comments: comment.comment, + }); + expect(res).toEqual([ + { commentId: '123', pushedDate: '2020-03-10T12:24:20.000Z' }, + { commentId: '456', pushedDate: '2020-03-10T12:25:20.000Z' }, + ]); + }); + }); + + test('throw if not status is not ok', async () => { + expect.assertions(1); + + axiosMock.mockResolvedValue({ + status: 401, + headers: { + 'content-type': 'application/json', + }, + }); + try { + await serviceNow.getUserID(); + } catch (error) { + expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + } + }); + + test('throw if not content-type is not application/json', async () => { + expect.assertions(1); + + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/html', + }, + }); + try { + await serviceNow.getUserID(); + } catch (error) { + expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + } + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts new file mode 100644 index 0000000000000..b3d17affb14c2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; + +import { INCIDENT_URL, USER_URL, COMMENT_URL } from './constants'; +import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; +import { CommentType } from '../types'; + +const validStatusCodes = [200, 201]; + +class ServiceNow { + private readonly incidentUrl: string; + private readonly commentUrl: string; + private readonly userUrl: string; + private readonly axios: AxiosInstance; + + constructor(private readonly instance: Instance) { + if ( + !this.instance || + !this.instance.url || + !this.instance.username || + !this.instance.password + ) { + throw Error('[Action][ServiceNow]: Wrong configuration.'); + } + + this.incidentUrl = `${this.instance.url}/${INCIDENT_URL}`; + this.commentUrl = `${this.instance.url}/${COMMENT_URL}`; + this.userUrl = `${this.instance.url}/${USER_URL}`; + this.axios = axios.create({ + auth: { username: this.instance.username, password: this.instance.password }, + }); + } + + private _throwIfNotAlive(status: number, contentType: string) { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('[ServiceNow]: Instance is not alive.'); + } + } + + private async _request({ + url, + method = 'get', + data = {}, + }: { + url: string; + method?: Method; + data?: any; + }): Promise { + const res = await this.axios(url, { method, data }); + this._throwIfNotAlive(res.status, res.headers['content-type']); + return res; + } + + private _patch({ url, data }: { url: string; data: any }): Promise { + return this._request({ + url, + method: 'patch', + data, + }); + } + + private _addTimeZoneToDate(date: string, timezone = 'GMT'): string { + return `${date} GMT`; + } + + async getUserID(): Promise { + const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); + return res.data.result[0].sys_id; + } + + async createIncident(incident: Incident): Promise { + const res = await this._request({ + url: `${this.incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + }; + } + + async updateIncident(incidentId: string, incident: UpdateIncident): Promise { + const res = await this._patch({ + url: `${this.incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } + + async batchCreateComments( + incidentId: string, + comments: CommentType[], + field: string + ): Promise { + const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field))); + return res; + } + + async createComment( + incidentId: string, + comment: CommentType, + field: string + ): Promise { + const res = await this._patch({ + url: `${this.commentUrl}/${incidentId}`, + data: { [field]: comment.comment }, + }); + + return { + commentId: comment.commentId, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } +} + +export { ServiceNow }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts new file mode 100644 index 0000000000000..4a3c5c42fcb44 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface Instance { + url: string; + username: string; + password: string; +} + +export interface Incident { + short_description?: string; + description?: string; + caller_id?: string; +} + +export interface IncidentResponse { + number: string; + incidentId: string; + pushedDate: string; +} + +export interface CommentResponse { + commentId: string; + pushedDate: string; +} + +export type UpdateIncident = Partial; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts new file mode 100644 index 0000000000000..9a150bbede5f8 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MapsType, FinalMapping, ParamsType } from './types'; +import { Incident } from './lib/types'; + +const mapping: MapsType[] = [ + { source: 'title', target: 'short_description', actionType: 'nothing' }, + { source: 'description', target: 'description', actionType: 'nothing' }, + { source: 'comments', target: 'comments', actionType: 'nothing' }, +]; + +const finalMapping: FinalMapping = new Map(); + +finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', +}); + +finalMapping.set('description', { + target: 'description', + actionType: 'nothing', +}); + +finalMapping.set('comments', { + target: 'comments', + actionType: 'nothing', +}); + +finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', +}); + +const params: ParamsType = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: '263ede42075300100e48fbbf7c1ed047', + }, + { + commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', + version: 'WlK3LDFd', + comment: 'Another comment', + incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + }, + ], +}; + +const incidentResponse = { + incidentId: 'c816f79cc0a8016401c5a33be04be441', + number: 'INC0010001', +}; + +const userId = '2e9a0a5e2f79001016ab51172799b670'; + +const axiosResponse = { + status: 200, + headers: { + 'content-type': 'application/json', + }, +}; +const userIdResponse = { + result: [{ sys_id: userId }], +}; + +const incidentAxiosResponse = { + result: { sys_id: incidentResponse.incidentId, number: incidentResponse.number }, +}; + +const instance = { + url: 'https://instance.service-now.com', + username: 'username', + password: 'password', +}; + +const incident: Incident = { + short_description: params.title, + description: params.description, + caller_id: userId, +}; + +export { + mapping, + finalMapping, + params, + incidentResponse, + incidentAxiosResponse, + userId, + userIdResponse, + axiosResponse, + instance, + incident, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts new file mode 100644 index 0000000000000..0bb4f50819665 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const MapsSchema = schema.object({ + source: schema.string(), + target: schema.string(), + actionType: schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), + ]), +}); + +export const CasesConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapsSchema), +}); + +export const ConfigSchemaProps = { + apiUrl: schema.string(), + casesConfiguration: CasesConfigurationSchema, +}; + +export const ConfigSchema = schema.object(ConfigSchemaProps); + +export const SecretsSchemaProps = { + password: schema.string(), + username: schema.string(), +}; + +export const SecretsSchema = schema.object(SecretsSchemaProps); + +export const CommentSchema = schema.object({ + commentId: schema.string(), + comment: schema.string(), + version: schema.maybe(schema.string()), + incidentCommentId: schema.maybe(schema.string()), +}); + +export const ExecutorAction = schema.oneOf([ + schema.literal('newIncident'), + schema.literal('updateIncident'), +]); + +export const ParamsSchema = schema.object({ + caseId: schema.string(), + comments: schema.maybe(schema.arrayOf(CommentSchema)), + description: schema.maybe(schema.string()), + title: schema.maybe(schema.string()), + incidentId: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts new file mode 100644 index 0000000000000..8601c5ce772db --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.actions.builtin.servicenow.servicenowApiNullError', + { + defaultMessage: 'ServiceNow [apiUrl] is required', + } +); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { + defaultMessage: 'error configuring servicenow action: {message}', + values: { + message, + }, + }); + +export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { + defaultMessage: 'ServiceNow', +}); + +export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', { + defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', +}); + +export const ERROR_POSTING = i18n.translate( + 'xpack.actions.builtin.servicenow.postingErrorMessage', + { + defaultMessage: 'error posting servicenow event', + } +); + +export const RETRY_POSTING = (status: number) => + i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { + defaultMessage: 'error posting servicenow event: http status {status}, retry later', + values: { + status, + }, + }); + +export const UNEXPECTED_STATUS = (status: number) => + i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { + defaultMessage: 'error posting servicenow event: unexpected status {status}', + values: { + status, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts new file mode 100644 index 0000000000000..7442f14fed064 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; + +import { + ConfigSchema, + SecretsSchema, + ParamsSchema, + CasesConfigurationSchema, + MapsSchema, + CommentSchema, +} from './schema'; + +import { ServiceNow } from './lib'; + +// config definition +export type ConfigType = TypeOf; + +// secrets definition +export type SecretsType = TypeOf; + +export type ParamsType = TypeOf; + +export type CasesConfigurationType = TypeOf; +export type MapsType = TypeOf; +export type CommentType = TypeOf; + +export type FinalMapping = Map; + +export interface ActionHandlerArguments { + serviceNow: ServiceNow; + params: any; + comments: CommentType[]; + mapping: FinalMapping; +} + +export type UpdateParamsType = Partial; +export type UpdateActionHandlerArguments = ActionHandlerArguments & { + incidentId: string; +}; + +export interface IncidentCreationResponse { + incidentId: string; + number: string; + comments?: CommentsZipped[]; + pushedDate: string; +} + +export interface CommentsZipped { + commentId: string; + pushedDate: string; +} diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 6e12ffb6404c6..69b357196dc32 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -30,7 +30,7 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider; diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index 0d0ddc55a391b..0c77b446fa28d 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -5,5 +5,6 @@ "server": true, "ui": true, "requiredPlugins": ["licensing"], - "optionalPlugins": ["home"] + "optionalPlugins": ["home"], + "configPath": ["xpack", "graph"] } diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts deleted file mode 100644 index ef5cffc05d8d7..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const TEMPLATE_NAME = 'my_template'; - -export const INDEX_PATTERNS = ['my_index_pattern']; - -export const SETTINGS = { - number_of_shards: 1, - index: { - lifecycle: { - name: 'my_policy', - }, - }, -}; - -export const ALIASES = { - alias: { - filter: { - term: { user: 'my_user' }, - }, - }, -}; - -export const MAPPINGS = { - properties: {}, -}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts deleted file mode 100644 index 7e3e1fba9c44a..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ReactWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import { - registerTestBed, - TestBed, - TestBedConfig, - findTestSubject, - nextTick, -} from '../../../../../test_utils'; -import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { BASE_PATH } from '../../../common/constants'; -import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { Template } from '../../../common/types'; -import { WithAppDependencies, services } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - store: () => indexManagementStore(services as any), - memoryRouter: { - initialEntries: [`${BASE_PATH}indices`], - componentRoutePath: `${BASE_PATH}:section(indices|templates)`, - }, - doMountAsync: true, -}; - -const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); - -export interface IdxMgmtHomeTestBed extends TestBed { - findAction: (action: 'edit' | 'clone' | 'delete') => ReactWrapper; - actions: { - selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void; - selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void; - clickReloadButton: () => void; - clickTemplateAction: (name: Template['name'], action: 'edit' | 'clone' | 'delete') => void; - clickTemplateAt: (index: number) => void; - clickCloseDetailsButton: () => void; - clickActionMenu: (name: Template['name']) => void; - }; -} - -export const setup = async (): Promise => { - const testBed = await initTestBed(); - - /** - * Additional helpers - */ - const findAction = (action: 'edit' | 'clone' | 'delete') => { - const actions = ['edit', 'clone', 'delete']; - const { component } = testBed; - - return component.find('.euiContextMenuItem').at(actions.indexOf(action)); - }; - - /** - * User Actions - */ - - const selectHomeTab = (tab: 'indicesTab' | 'templatesTab') => { - testBed.find(tab).simulate('click'); - }; - - const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => { - const tabs = ['summary', 'settings', 'mappings', 'aliases']; - - testBed - .find('templateDetails.tab') - .at(tabs.indexOf(tab)) - .simulate('click'); - }; - - const clickReloadButton = () => { - const { find } = testBed; - find('reloadButton').simulate('click'); - }; - - const clickActionMenu = async (templateName: Template['name']) => { - const { component } = testBed; - - // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions" - // The template name may contain a period (.) so we use bracket syntax for selector - component.find(`div[id="${templateName}-actions"] button`).simulate('click'); - }; - - const clickTemplateAction = ( - templateName: Template['name'], - action: 'edit' | 'clone' | 'delete' - ) => { - const actions = ['edit', 'clone', 'delete']; - const { component } = testBed; - - clickActionMenu(templateName); - - component - .find('.euiContextMenuItem') - .at(actions.indexOf(action)) - .simulate('click'); - }; - - const clickTemplateAt = async (index: number) => { - const { component, table, router } = testBed; - const { rows } = table.getMetaData('templateTable'); - const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); - - await act(async () => { - const { href } = templateLink.props(); - router.navigateTo(href!); - await nextTick(); - component.update(); - }); - }; - - const clickCloseDetailsButton = () => { - const { find } = testBed; - - find('closeDetailsButton').simulate('click'); - }; - - return { - ...testBed, - findAction, - actions: { - selectHomeTab, - selectDetailsTab, - clickReloadButton, - clickTemplateAction, - clickTemplateAt, - clickCloseDetailsButton, - clickActionMenu, - }, - }; -}; - -type IdxMgmtTestSubjects = TestSubjects; - -export type TestSubjects = - | 'aliasesTab' - | 'appTitle' - | 'cell' - | 'closeDetailsButton' - | 'createTemplateButton' - | 'deleteSystemTemplateCallOut' - | 'deleteTemplateButton' - | 'deleteTemplatesConfirmation' - | 'documentationLink' - | 'emptyPrompt' - | 'manageTemplateButton' - | 'mappingsTab' - | 'noAliasesCallout' - | 'noMappingsCallout' - | 'noSettingsCallout' - | 'indicesList' - | 'indicesTab' - | 'reloadButton' - | 'row' - | 'sectionError' - | 'sectionLoading' - | 'settingsTab' - | 'summaryTab' - | 'summaryTitle' - | 'systemTemplatesSwitch' - | 'templateDetails' - | 'templateDetails.manageTemplateButton' - | 'templateDetails.sectionLoading' - | 'templateDetails.tab' - | 'templateDetails.title' - | 'templateList' - | 'templateTable' - | 'templatesTab'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts deleted file mode 100644 index e5bce31ee6de1..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import sinon, { SinonFakeServer } from 'sinon'; -import { API_BASE_PATH } from '../../../common/constants'; - -type HttpResponse = Record | any[]; - -// Register helpers to mock HTTP Requests -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setLoadTemplatesResponse = (response: HttpResponse = []) => { - server.respondWith('GET', `${API_BASE_PATH}/templates`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setLoadIndicesResponse = (response: HttpResponse = []) => { - server.respondWith('GET', `${API_BASE_PATH}/indices`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setDeleteTemplateResponse = (response: HttpResponse = []) => { - server.respondWith('DELETE', `${API_BASE_PATH}/templates`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setLoadTemplateResponse = (response?: HttpResponse, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? error.body : response; - - server.respondWith('GET', `${API_BASE_PATH}/templates/:id`, [ - status, - { 'Content-Type': 'application/json' }, - JSON.stringify(body), - ]); - }; - - const setCreateTemplateResponse = (response?: HttpResponse, error?: any) => { - const status = error ? error.body.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - - server.respondWith('PUT', `${API_BASE_PATH}/templates`, [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); - }; - - const setUpdateTemplateResponse = (response?: HttpResponse, error?: any) => { - const status = error ? error.status || 400 : 200; - const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - - server.respondWith('PUT', `${API_BASE_PATH}/templates/:name`, [ - status, - { 'Content-Type': 'application/json' }, - body, - ]); - }; - - return { - setLoadTemplatesResponse, - setLoadIndicesResponse, - setDeleteTemplateResponse, - setLoadTemplateResponse, - setCreateTemplateResponse, - setUpdateTemplateResponse, - }; -}; - -export const init = () => { - const server = sinon.fakeServer.create(); - server.respondImmediately = true; - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); - - return { - server, - httpRequestsMockHelpers, - }; -}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts deleted file mode 100644 index 66021b531919a..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { setup as homeSetup } from './home.helpers'; -import { setup as templateCreateSetup } from './template_create.helpers'; -import { setup as templateCloneSetup } from './template_clone.helpers'; -import { setup as templateEditSetup } from './template_edit.helpers'; - -export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../test_utils'; - -export { setupEnvironment } from './setup_environment'; - -export const pageHelpers = { - home: { setup: homeSetup }, - templateCreate: { setup: templateCreateSetup }, - templateClone: { setup: templateCloneSetup }, - templateEdit: { setup: templateEditSetup }, -}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx deleted file mode 100644 index 1eaf7efd17395..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import React from 'react'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; - -import { - notificationServiceMock, - docLinksServiceMock, -} from '../../../../../../src/core/public/mocks'; -import { AppContextProvider } from '../../../public/application/app_context'; -import { httpService } from '../../../public/application/services/http'; -import { breadcrumbService } from '../../../public/application/services/breadcrumbs'; -import { documentationService } from '../../../public/application/services/documentation'; -import { notificationService } from '../../../public/application/services/notification'; -import { ExtensionsService } from '../../../public/services'; -import { UiMetricService } from '../../../public/application/services/ui_metric'; -import { setUiMetricService } from '../../../public/application/services/api'; -import { setExtensionsService } from '../../../public/application/store/selectors'; -import { init as initHttpRequests } from './http_requests'; - -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - -export const services = { - extensionsService: new ExtensionsService(), - uiMetricService: new UiMetricService('index_management'), -}; -services.uiMetricService.setup({ reportUiStats() {} } as any); -setExtensionsService(services.extensionsService); -setUiMetricService(services.uiMetricService); -const appDependencies = { services, core: {}, plugins: {} } as any; - -export const setupEnvironment = () => { - // Mock initialization of services - // @ts-ignore - httpService.setup(mockHttpClient); - breadcrumbService.setup(() => undefined); - documentationService.setup(docLinksServiceMock.createStartContract()); - notificationService.setup(notificationServiceMock.createSetupContract()); - - const { server, httpRequestsMockHelpers } = initHttpRequests(); - - return { - server, - httpRequestsMockHelpers, - }; -}; - -export const WithAppDependencies = (Comp: any) => (props: any) => ( - - - -); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts deleted file mode 100644 index 36498b99ba143..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; -import { TemplateClone } from '../../../public/application/sections/template_clone'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { formSetup } from './template_form.helpers'; -import { TEMPLATE_NAME } from './constants'; -import { WithAppDependencies } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: [`${BASE_PATH}clone_template/${TEMPLATE_NAME}`], - componentRoutePath: `${BASE_PATH}clone_template/:name`, - }, - doMountAsync: true, -}; - -const initTestBed = registerTestBed(WithAppDependencies(TemplateClone), testBedConfig); - -export const setup = formSetup.bind(null, initTestBed); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts deleted file mode 100644 index 14a44968a93c3..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; -import { TemplateCreate } from '../../../public/application/sections/template_create'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { formSetup, TestSubjects } from './template_form.helpers'; -import { WithAppDependencies } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: [`${BASE_PATH}create_template`], - componentRoutePath: `${BASE_PATH}create_template`, - }, - doMountAsync: true, -}; - -const initTestBed = registerTestBed( - WithAppDependencies(TemplateCreate), - testBedConfig -); - -export const setup = formSetup.bind(null, initTestBed); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts deleted file mode 100644 index af5fa8b79ecad..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; -import { TemplateEdit } from '../../../public/application/sections/template_edit'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { formSetup, TestSubjects } from './template_form.helpers'; -import { TEMPLATE_NAME } from './constants'; -import { WithAppDependencies } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: [`${BASE_PATH}edit_template/${TEMPLATE_NAME}`], - componentRoutePath: `${BASE_PATH}edit_template/:name`, - }, - doMountAsync: true, -}; - -const initTestBed = registerTestBed(WithAppDependencies(TemplateEdit), testBedConfig); - -export const setup = formSetup.bind(null, initTestBed); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts deleted file mode 100644 index 134c67c278b22..0000000000000 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils'; -import { Template } from '../../../common/types'; -import { nextTick } from './index'; - -interface MappingField { - name: string; - type: string; -} - -// Look at the return type of formSetup and form a union between that type and the TestBed type. -// This way we an define the formSetup return object and use that to dynamically define our type. -export type TemplateFormTestBed = TestBed & - UnwrapPromise>; - -export const formSetup = async (initTestBed: SetupFunc) => { - const testBed = await initTestBed(); - - // User actions - const clickNextButton = () => { - testBed.find('nextButton').simulate('click'); - }; - - const clickBackButton = () => { - testBed.find('backButton').simulate('click'); - }; - - const clickSubmitButton = () => { - testBed.find('submitButton').simulate('click'); - }; - - const clickEditButtonAtField = (index: number) => { - testBed - .find('editFieldButton') - .at(index) - .simulate('click'); - }; - - const clickEditFieldUpdateButton = () => { - testBed.find('editFieldUpdateButton').simulate('click'); - }; - - const clickRemoveButtonAtField = (index: number) => { - testBed - .find('removeFieldButton') - .at(index) - .simulate('click'); - - testBed.find('confirmModalConfirmButton').simulate('click'); - }; - - const clickCancelCreateFieldButton = () => { - testBed.find('createFieldWrapper.cancelButton').simulate('click'); - }; - - const completeStepOne = async ({ - name, - indexPatterns, - order, - version, - }: Partial