diff --git a/docs/maps/heatmap-layer.asciidoc b/docs/maps/heatmap-layer.asciidoc index 77b6d929a931c..7149bc5623169 100644 --- a/docs/maps/heatmap-layer.asciidoc +++ b/docs/maps/heatmap-layer.asciidoc @@ -2,15 +2,12 @@ [[heatmap-layer]] == Heat map layer -In the heat map layer, point data is clustered to show locations with higher densities. +Heat map layers cluster point data to show locations with higher densities. [role="screenshot"] image::maps/images/heatmap_layer.png[] -You can create a heat map layer from the following data source: - -*Grid aggregation*:: Geospatial data grouped in grids with metrics for each gridded cell. -Set *Show as* to *heat map*. +To add a heat map layer to your map, click *Add layer*, then select the *Heat map* layer. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point]. NOTE: Only count, sum, unique count metric aggregations are available with the grid aggregation source and heat map layers. diff --git a/docs/maps/images/heatmap_layer.png b/docs/maps/images/heatmap_layer.png index 8d59de38beccd..87a45146f95a5 100644 Binary files a/docs/maps/images/heatmap_layer.png and b/docs/maps/images/heatmap_layer.png differ diff --git a/docs/maps/images/spatial_filters.png b/docs/maps/images/spatial_filters.png new file mode 100644 index 0000000000000..991e7f62962d0 Binary files /dev/null and b/docs/maps/images/spatial_filters.png differ diff --git a/docs/maps/images/tile_layer.png b/docs/maps/images/tile_layer.png index 60cb90ac5b90b..fc1d571b3e9b0 100644 Binary files a/docs/maps/images/tile_layer.png and b/docs/maps/images/tile_layer.png differ diff --git a/docs/maps/images/vector_layer.png b/docs/maps/images/vector_layer.png index a30f6c1d6acfd..6bc9701759ce7 100644 Binary files a/docs/maps/images/vector_layer.png and b/docs/maps/images/vector_layer.png differ diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index 56826c5209034..de90d7adb29c0 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -30,6 +30,7 @@ include::tile-layer.asciidoc[] include::vector-layer.asciidoc[] include::maps-aggregations.asciidoc[] include::search.asciidoc[] +include::map-settings.asciidoc[] include::connect-to-ems.asciidoc[] include::geojson-upload.asciidoc[] include::indexing-geojson-data-tutorial.asciidoc[] diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index bf846a2b80e03..c1ca9d0925c9a 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -87,14 +87,13 @@ hot spots are. An advantage of having indexed lightning strikes is that you can perform aggregations on the data. . Click *Add layer*. -. From the list of layer types, click *Grid aggregation*. +. From the list of layer types, click *Heat map*. + Because you indexed `lightning_detected.geojson` using the index name and pattern `lightning_detected`, that data is available as a {ref}/geo-point.html[geo_point] aggregation. . Select `lightning_detected`. -. Click *Show as* and select `heat map`. . Click *Add layer* to add the heat map layer "Lightning intensity". + diff --git a/docs/maps/map-settings.asciidoc b/docs/maps/map-settings.asciidoc new file mode 100644 index 0000000000000..4e290b6da2e71 --- /dev/null +++ b/docs/maps/map-settings.asciidoc @@ -0,0 +1,39 @@ +[role="xpack"] +[[maps-settings]] +== Map settings + +Elastic Maps offers settings that let you configure how a map is displayed. +To access these settings, click *Map settings* in the application toolbar. + +[float] +[[maps-settings-navigation]] +=== Navigation + +*Zoom range*:: +Constrain the map to the defined zoom range. + +*Initial map location*:: +Configure the initial map center and zoom. +* *Map location at save*: Use the map center and zoom from the map position at the time of the latest save. +* *Fixed location*: Lock the map center and zoom to fixed values. +* *Browser location*: Set the initial map center to the browser location. + +[float] +[[maps-settings-spatial-filters]] +=== Spatial filters + +Use spatial filter settings to configure how <> are displayed. + +image::maps/images/spatial_filters.png[] + +*Show spatial filters on map*:: +Clear the checkbox so <> do not appear on the map. + +*Opacity*:: +Set the opacity of spatial filters. + +*Fill color*:: +Set the fill color of spatial filters. + +*Border color*:: +Set the border color of spatial filters. diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index 2b65ae99a381b..6b03614ab9d6a 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -37,7 +37,7 @@ image::maps/images/grid_to_docs.gif[] [[maps-grid-aggregation]] === Grid aggregation -The *Grid aggregation* source uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell. +*Grid aggregation* layers use {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell. Symbolize grid aggregation metrics as: @@ -48,13 +48,13 @@ The cluster location is the weighted centroid for all geo-points in the gridded *Heat map*:: Creates a <> that clusters the weighted centroids for each gridded cell. -To enable grid aggregation: +To enable a grid aggregation layer: -. Click *Add layer*, then select the *Grid aggregation* source. +. Click *Add layer*, then select the *Clusters and grids* or *Heat map* layer. To enable a blended layer that dynamically shows clusters or documents: -. Click *Add layer*, then select the *Documents* source. +. Click *Add layer*, then select the *Documents* layer. . Configure *Index pattern* and the *Geospatial field*. To enable clustering, the *Geospatial field* must be set to a field mapped as {ref}/geo-point.html[geo_point]. . In *Scaling*, select *Show clusters when results exceed 10000*. @@ -69,7 +69,7 @@ then accumulates the most relevant documents based on sort order for each entry To enable top hits: -. Click *Add layer* button and select *Documents* source. +. Click *Add layer*, then select the *Documents* layer. . Configure *Index pattern* and *Geospatial field*. . In *Scaling*, select *Show top hits per entity*. . Set *Entity* to the field that identifies entities in your documents. @@ -99,7 +99,7 @@ image::maps/images/point_to_point.png[] Use term joins to augment vector features with properties for <> and richer tooltip content. -Term joins are available for <> with the following sources: +Term joins are available for the following <>: * Configured GeoJSON * Documents diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 6495b8a057cf6..a74d442d6ffa2 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -62,10 +62,10 @@ The first layer you'll add is a choropleth layer to shade world countries by web log traffic. Darker shades symbolize countries with more web log traffic, and lighter shades symbolize countries with less traffic. -==== Add a vector layer from the Elastic Maps Service source +==== Add a vector layer to display world country boundaries -. In the map legend, click *Add layer*. -. Click the *EMS Boundaries* data source. +. Click *Add layer*. +. Select the *EMS Boundaries* layer. . From the *Layer* dropdown menu, select *World Countries*. . Click the *Add layer* button. . Set *Name* to `Total Requests by Country`. @@ -112,16 +112,16 @@ To avoid overwhelming the user with too much data at once, you'll add two layers * The first layer will display individual documents. The layer will appear when the user zooms in the map to show smaller regions. -* The second layer will show aggregated data that represents many documents. +* The second layer will display aggregated data that represents many documents. The layer will appear when the user zooms out the map to show larger amounts of the globe. -==== Add a vector layer from the document source +==== Add a vector layer to display individual documents This layer displays web log documents as points. The layer is only visible when users zoom in the map past zoom level 9. -. In the map legend, click *Add layer*. -. Click the *Documents* data source. +. Click *Add layer*. +. Click the *Documents* layer. . Set *Index pattern* to *kibana_sample_data_logs*. . Click the *Add layer* button. . Set *Name* to `Actual Requests`. @@ -137,7 +137,7 @@ Your map now looks like this between zoom levels 9 and 24: [role="screenshot"] image::maps/images/gs_add_es_document_layer.png[] -==== Add a vector layer from the grid aggregation source +==== Add a vector layer to display aggregated data Aggregations group {es} documents into grids. You can calculate metrics for each gridded cell. @@ -154,10 +154,9 @@ image::maps/images/grid_metrics_both.png[] ===== Add the layer -. In the map legend, click *Add layer*. -. Click the *Grid aggregation* data source. +. Click *Add layer*. +. Click the *Clusters and grids* layer. . Set *Index pattern* to *kibana_sample_data_logs*. -. Set *Show as* to *clusters*. . Click the *Add layer* button. . Set *Name* to `Total Requests and Bytes`. . Set *Visibility* to the range [0, 9]. diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index a461ab6fbb3a6..124a976c009d4 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -10,13 +10,13 @@ You can create a layer that requests data from {es} from the following: * <> with: -** Documents source +** Documents -** Grid aggregation source +** Clusters and grid ** <> -* <> with Grid aggregation source +* <> [role="screenshot"] image::maps/images/global_search_bar.png[] diff --git a/docs/maps/tile-layer.asciidoc b/docs/maps/tile-layer.asciidoc index 059dd527f4810..6da8dbad0a66d 100644 --- a/docs/maps/tile-layer.asciidoc +++ b/docs/maps/tile-layer.asciidoc @@ -2,12 +2,12 @@ [[tile-layer]] == Tile layer -The tile layer displays image tiles served from a tile server. +Tile layers display image tiles served from a tile server. [role="screenshot"] image::maps/images/tile_layer.png[] -You can create a tile layer from the following data sources: +To add a tile layer to your map, click *Add layer*, then select one of the following layers: *Configured Tile Map Service*:: Tile map service configured in kibana.yml. See map.tilemap.url in <> for details. diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 17c57c82b0f17..d6a5931659a40 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -2,12 +2,15 @@ [[vector-layer]] == Vector layer -The vector layer displays points, lines, and polygons. +Vector layers display points, lines, and polygons. [role="screenshot"] image::maps/images/vector_layer.png[] -You can create a vector layer from the following sources: +To add a vector layer to your map, click *Add layer*, then select one of the following layers: + +*Clusters and grids*:: Geospatial data grouped in grids with metrics for each gridded cell. +The index must contain at least one field mapped as {ref}/geo-point.html[geo_point]. *Configured GeoJSON*:: Vector data from hosted GeoJSON configured in kibana.yml. See map.regionmap.* in <> for details. @@ -18,15 +21,13 @@ The index must contain at least one field mapped as {ref}/geo-point.html[geo_poi NOTE: Document results are limited to the `index.max_result_window` index setting, which defaults to 10000. Use <> to plot large data sets. -*Grid aggregation*:: Geospatial data grouped in grids with metrics for each gridded cell. -Set *Show as* to *grid rectangles* or *clusters*. -The index must contain at least one field mapped as {ref}/geo-point.html[geo_point]. - *EMS Boundaries*:: Administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. *Point to point*:: Aggregated data paths between the source and destination. The index must contain at least 2 fields mapped as {ref}/geo-point.html[geo_point], source and destination. +*Upload Geojson*:: Index GeoJSON data in Elasticsearch. + include::vector-style.asciidoc[] include::vector-style-properties.asciidoc[] include::vector-tooltips.asciidoc[] diff --git a/docs/maps/vector-style.asciidoc b/docs/maps/vector-style.asciidoc index 7bc8a909d1ec6..5f5b3a1b2aecd 100644 --- a/docs/maps/vector-style.asciidoc +++ b/docs/maps/vector-style.asciidoc @@ -86,7 +86,7 @@ Qualitative data driven styling is available for the following styling propertie * *Label color* * *Label border color* -To ensure symbols are consistent as you pan, zoom, and filter the map, qualitative data driven styling uses a {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation]. The term aggregation retrieves the top nine categories for the property. Feature values within the top categories are assigned a unique style. Feature values outside of the top categories are grouped into the *Other* category. A feature is assigned the *Other* category when the property value is undefined. +To ensure symbols are consistent as you pan, zoom, and filter the map, qualitative data driven styling uses a {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation]. The term aggregation retrieves the top categories for the property. Feature values within the top categories are assigned a unique style. Feature values outside of the top categories are grouped into the *Other* category. A feature is assigned the *Other* category when the property value is undefined. To configure the terms aggregation, click the gear icon image:maps/images/gear_icon.png[]. Clear the *Get categories from indice* checkbox to turn off the terms aggregation request. diff --git a/packages/kbn-es/src/utils/native_realm.js b/packages/kbn-es/src/utils/native_realm.js index 247ddc461910f..086898abb6b67 100644 --- a/packages/kbn-es/src/utils/native_realm.js +++ b/packages/kbn-es/src/utils/native_realm.js @@ -76,6 +76,10 @@ exports.NativeRealm = class NativeRealm { } const reservedUsers = await this.getReservedUsers(); + if (!reservedUsers || reservedUsers.length < 1) { + throw new Error('no reserved users found, unable to set native realm passwords'); + } + await Promise.all( reservedUsers.map(async user => { await this.setPassword(user, options[`password.${user}`]); diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts index 5e1a3794632ee..009debd928d43 100644 --- a/src/core/server/core_app/core_app.ts +++ b/src/core/server/core_app/core_app.ts @@ -52,6 +52,17 @@ export class CoreApp { router.get({ path: '/core', validate: false }, async (context, req, res) => res.ok({ body: { version: '0.0.1' } }) ); + + coreSetup.savedObjects.registerType({ + name: 'server', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + uuid: { type: 'keyword' }, + }, + }, + }); } private registerStaticDirs(coreSetup: InternalCoreSetup) { coreSetup.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets')); diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 05840926d35de..d9d0528748dc0 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -126,6 +126,7 @@ export PATH="$PATH:$yarnGlobalDir" # use a proxy to fetch chromedriver/geckodriver asset export GECKODRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CHROMEDRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" +export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress" export CHECKS_REPORTER_ACTIVE=false diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 6664cf0d7366d..3a796b3ba520c 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -23,7 +23,6 @@ import { promisify } from 'util'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; -import mappings from './mappings.json'; import { getUiSettingDefaults } from './server/ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; import { injectVars } from './inject_vars'; @@ -138,7 +137,6 @@ export default function(kibana) { }; }, - mappings, uiSettingDefaults: getUiSettingDefaults(), }, diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json deleted file mode 100644 index e2cbd584dbe1f..0000000000000 --- a/src/legacy/core_plugins/kibana/mappings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - } -} diff --git a/src/legacy/core_plugins/timelion/index.ts b/src/legacy/core_plugins/timelion/index.ts index 31926f658ec13..9c8ab156d1a79 100644 --- a/src/legacy/core_plugins/timelion/index.ts +++ b/src/legacy/core_plugins/timelion/index.ts @@ -62,7 +62,6 @@ const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPl }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), hacks: [resolve(__dirname, 'public/legacy')], - mappings: require('./mappings.json'), uiSettingDefaults: { 'timelion:showTutorial': { name: i18n.translate('timelion.uiSettings.showTutorialLabel', { diff --git a/src/legacy/core_plugins/timelion/mappings.json b/src/legacy/core_plugins/timelion/mappings.json deleted file mode 100644 index eb761cfe46212..0000000000000 --- a/src/legacy/core_plugins/timelion/mappings.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - } -} diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json new file mode 100644 index 0000000000000..55e492e8f23cd --- /dev/null +++ b/src/plugins/timelion/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "timelion", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": "timelion", + "ui": false, + "server": true +} diff --git a/src/plugins/timelion/server/index.ts b/src/plugins/timelion/server/index.ts new file mode 100644 index 0000000000000..5bb0c9e2567e0 --- /dev/null +++ b/src/plugins/timelion/server/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { TimelionPlugin } from './plugin'; + +export const plugin = (context: PluginInitializerContext) => new TimelionPlugin(context); diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts new file mode 100644 index 0000000000000..015f0c573e531 --- /dev/null +++ b/src/plugins/timelion/server/plugin.ts @@ -0,0 +1,52 @@ +/* + * 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 { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; + +export class TimelionPlugin implements Plugin { + constructor(context: PluginInitializerContext) {} + + setup(core: CoreSetup) { + core.savedObjects.registerType({ + name: 'timelion-sheet', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + description: { type: 'text' }, + hits: { type: 'integer' }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { type: 'text' }, + }, + }, + timelion_chart_height: { type: 'integer' }, + timelion_columns: { type: 'integer' }, + timelion_interval: { type: 'keyword' }, + timelion_other_interval: { type: 'keyword' }, + timelion_rows: { type: 'integer' }, + timelion_sheet: { type: 'text' }, + title: { type: 'text' }, + version: { type: 'integer' }, + }, + }, + }); + } + start() {} + stop() {} +} diff --git a/test/functional/apps/context/_context_navigation.js b/test/functional/apps/context/_context_navigation.js index e4066d4159b9e..b2312691599ba 100644 --- a/test/functional/apps/context/_context_navigation.js +++ b/test/functional/apps/context/_context_navigation.js @@ -30,7 +30,8 @@ export default function({ getService, getPageObjects }) { const docTable = getService('docTable'); const PageObjects = getPageObjects(['common', 'context', 'discover', 'timePicker']); - describe('context link in discover', function contextSize() { + // FLAKY: https://github.com/elastic/kibana/issues/62866 + describe.skip('context link in discover', function contextSize() { before(async function() { await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index a46cdfe35e32d..5aa8ff302bc10 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -4,18 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import mappings from './mappings.json'; import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; -import { migrations } from './migrations'; import { getAppTitle } from '../../../plugins/maps/common/i18n_getters'; import { MapPlugin } from './server/plugin'; -import { - APP_ID, - APP_ICON, - createMapPath, - MAP_SAVED_OBJECT_TYPE, -} from '../../../plugins/maps/common/constants'; +import { APP_ID, APP_ICON } from '../../../plugins/maps/common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; export function maps(kibana) { @@ -48,29 +41,6 @@ export function maps(kibana) { }; }, styleSheetPaths: `${__dirname}/public/index.scss`, - savedObjectSchemas: { - 'maps-telemetry': { - isNamespaceAgnostic: true, - }, - }, - savedObjectsManagement: { - [MAP_SAVED_OBJECT_TYPE]: { - icon: APP_ICON, - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getInAppUrl(obj) { - return { - path: createMapPath(obj.id), - uiCapabilitiesPath: 'maps.show', - }; - }, - }, - }, - mappings, - migrations, }, config(Joi) { return Joi.object({ diff --git a/x-pack/legacy/plugins/maps/mappings.json b/x-pack/legacy/plugins/maps/mappings.json deleted file mode 100644 index c939d096d7849..0000000000000 --- a/x-pack/legacy/plugins/maps/mappings.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "map": { - "properties": { - "description": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "bounds": { - "type": "geo_shape" - }, - "mapStateJSON": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - } - } - }, - "maps-telemetry": { - "properties": { - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoPointFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoShapeFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "timeCaptured": { - "type": "date" - }, - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "min": { - "type": "long" - }, - "max": { - "type": "long" - }, - "avg": { - "type": "long" - } - } - }, - "layersCount": { - "properties": { - "min": { - "type": "long" - }, - "max": { - "type": "long" - }, - "avg": { - "type": "long" - } - } - }, - "layerTypesCount": { - "dynamic": "true", - "properties": {} - }, - "emsVectorLayersCount": { - "dynamic": "true", - "properties": {} - } - } - } - } - } -} diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts index 07d5fe59a9718..d3a978c9963cf 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -58,6 +58,7 @@ export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({ limit: rt.union([rt.number, rt.null, rt.undefined]), filterQuery: rt.union([rt.string, rt.null, rt.undefined]), forceInterval: rt.boolean, + dropLastBucket: rt.boolean, }); export const metricsExplorerRequestBodyRT = rt.intersection([ diff --git a/x-pack/plugins/infra/common/inventory_models/container/index.ts b/x-pack/plugins/infra/common/inventory_models/container/index.ts index c142f600d1d56..8f2336d11e42b 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/container/index.ts @@ -26,7 +26,7 @@ export const container: InventoryModel = { fields: { id: 'container.id', name: 'container.name', - ip: 'continaer.ip_address', + ip: 'container.ip_address', }, metrics, requiredMetrics: [ diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 77147d1b3b2b7..0e1195965448c 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -35,6 +35,7 @@ import { getChartTheme } from '../../../pages/metrics/metrics_explorer/component import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric'; import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain'; import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data'; +import { getMetricId } from '../../../pages/metrics/metrics_explorer/components/helpers/get_metric_id'; interface Props { context: AlertsContextValue; @@ -120,7 +121,7 @@ export const ExpressionChart: React.FC = ({ rows: firstSeries.rows.map(row => { const newRow: MetricsExplorerRow = { ...row }; thresholds.forEach((thresholdValue, index) => { - newRow[`metric_threshold_${index}`] = thresholdValue; + newRow[getMetricId(metric, `threshold_${index}`)] = thresholdValue; }); return newRow; }), @@ -140,7 +141,8 @@ export const ExpressionChart: React.FC = ({ const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(expression.comparator); const opacity = 0.3; - const timeLabel = TIME_LABELS[expression.timeUnit]; + const { timeSize, timeUnit } = expression; + const timeLabel = TIME_LABELS[timeUnit]; return ( <> @@ -255,8 +257,8 @@ export const ExpressionChart: React.FC = ({ ) : ( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 67f66bf742f43..185895062cfe2 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -26,6 +26,7 @@ export const useMetricsExplorerChartData = ( () => ({ limit: 1, forceInterval: true, + dropLastBucket: false, groupBy, filterQuery, metrics: [ diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx index 7f190f21484d9..fac1e086101e9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eui'; +import { EuiButton, EuiDatePicker, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; import React, { useCallback } from 'react'; +import { withTheme, EuiTheme } from '../../../../../../../observability/public'; import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; -export const WaffleTimeControls = () => { +interface Props { + theme: EuiTheme; +} + +export const WaffleTimeControls = withTheme(({ theme }: Props) => { const { currentTime, isAutoReloading, @@ -22,19 +27,19 @@ export const WaffleTimeControls = () => { const currentMoment = moment(currentTime); const liveStreamingButton = isAutoReloading ? ( - + - + ) : ( - + - + ); const handleChangeDate = useCallback( @@ -47,20 +52,31 @@ export const WaffleTimeControls = () => { ); return ( - - - + + + + + {liveStreamingButton} + ); -}; +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index 414e204f3df50..da6d77ef4b478 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -60,6 +60,7 @@ export function useMetricsExplorerData( method: 'POST', body: JSON.stringify({ forceInterval: options.forceInterval, + dropLastBucket: options.dropLastBucket != null ? options.dropLastBucket : true, metrics: options.aggregation === 'count' ? [{ aggregation: 'count' }] diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 1b3e809fde61f..f79c7aa0d4d67 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -41,6 +41,7 @@ export interface MetricsExplorerOptions { filterQuery?: string; aggregation: MetricsExplorerAggregation; forceInterval?: boolean; + dropLastBucket?: boolean; } export interface MetricsExplorerTimeOptions { diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts index a7f393261a096..3a9abf525a9f0 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts @@ -15,12 +15,15 @@ const percentileToVaue = (agg: 'p95' | 'p99') => { }; export const createMetricModel = (options: MetricsExplorerRequestBody): TSVBMetricModel => { + // if dropLastBucket is set use the value otherwise default to true. + const dropLastBucket: boolean = options.dropLastBucket != null ? options.dropLastBucket : true; return { id: 'custom', requires: [], index_pattern: options.indexPattern, interval: options.timerange.interval, time_field: options.timerange.field, + drop_last_bucket: dropLastBucket, type: 'timeseries', // Create one series per metric requested. The series.id will be used to identify the metric // when the responses are processed and combined with the grouping request. diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index ab0af94cbc2b4..d069a76f214d7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -236,17 +236,9 @@ export function XYChart({ // check all the tables to see if all of the rows have the same timestamp // that would mean that chart will draw a single bar const isSingleTimestampInXDomain = () => { - const nonEmptyLayers = layers.filter( - layer => data.tables[layer.layerId].rows.length && layer.xAccessor - ); - - if (!nonEmptyLayers.length) { - return; - } - const firstRowValue = - data.tables[nonEmptyLayers[0].layerId].rows[0][nonEmptyLayers[0].xAccessor!]; - for (const layer of nonEmptyLayers) { + data.tables[filteredLayers[0].layerId].rows[0][filteredLayers[0].xAccessor!]; + for (const layer of filteredLayers) { if ( layer.xAccessor && data.tables[layer.layerId].rows.some(row => row[layer.xAccessor!] !== firstRowValue) @@ -270,7 +262,7 @@ export function XYChart({ return undefined; } - const isTimeViz = data.dateRange && layers.every(l => l.xScaleType === 'time'); + const isTimeViz = data.dateRange && filteredLayers.every(l => l.xScaleType === 'time'); const xDomain = isTimeViz ? { @@ -299,12 +291,10 @@ export function XYChart({ return; } - const firstLayerWithData = - layers[layers.findIndex(layer => data.tables[layer.layerId].rows.length)]; - const table = data.tables[firstLayerWithData.layerId]; + const table = data.tables[filteredLayers[0].layerId]; const xAxisColumnIndex = table.columns.findIndex( - el => el.id === firstLayerWithData.xAccessor + el => el.id === filteredLayers[0].xAccessor ); const timeFieldName = table.columns[xAxisColumnIndex]?.meta?.aggConfigParams?.field; diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index 3bda29964a9a1..9f16b40eaf978 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -10,6 +10,7 @@ import { AnyAction } from 'redux'; import { LAYER_TYPE } from '../../common/constants'; import { DataMeta, + LayerDescriptor, MapFilters, MapCenterAndZoom, MapRefreshConfig, @@ -86,3 +87,13 @@ export function fitToLayerExtent(layerId: string): AnyAction; export function removeLayer(layerId: string): AnyAction; export function toggleLayerVisible(layerId: string): AnyAction; + +export function clearTransientLayerStateAndCloseFlyout(): AnyAction; + +export function setTransientLayer(layerId: string | null): AnyAction; + +export function removeTransientLayer(): AnyAction; + +export function addLayer(layerDescriptor: LayerDescriptor): AnyAction; + +export function setSelectedLayer(layerId: string | null): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/plugins/maps/public/actions/ui_actions.d.ts index 43cdcff7d2d69..066bae596e9db 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.d.ts @@ -5,7 +5,7 @@ */ import { AnyAction } from 'redux'; -import { FLYOUT_STATE } from '../reducers/ui'; +import { INDEXING_STAGE, FLYOUT_STATE } from '../reducers/ui'; export const UPDATE_FLYOUT: string; export const CLOSE_SET_VIEW: string; @@ -25,3 +25,5 @@ export function setOpenTOCDetails(layerIds?: string[]): AnyAction; export function setIsLayerTOCOpen(open: boolean): AnyAction; export function setReadOnly(readOnly: boolean): AnyAction; + +export function updateIndexingStage(state: INDEXING_STAGE | null): AnyAction; diff --git a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts index 7a2a038c1b286..dc5849203ff37 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts @@ -14,7 +14,7 @@ export type RenderWizardArguments = { // upload arguments isIndexingTriggered: boolean; onRemove: () => void; - onIndexReady: () => void; + onIndexReady: (indexReady: boolean) => void; importSuccessHandler: (indexResponses: unknown) => void; importErrorHandler: (indexResponses: unknown) => void; }; diff --git a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts index 590c1a9d6ae44..e81bce43133e4 100644 --- a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts @@ -5,7 +5,6 @@ */ import { registerLayerWizard } from './layer_wizard_registry'; -// @ts-ignore import { uploadLayerWizardConfig } from '../sources/client_file_source'; // @ts-ignore import { esDocumentsLayerWizardConfig } from '../sources/es_search_source'; @@ -35,7 +34,6 @@ export function registerLayerWizards() { } // Registration order determines display order - // @ts-ignore registerLayerWizard(uploadLayerWizardConfig); registerLayerWizard(ObservabilityLayerWizardConfig); // @ts-ignore diff --git a/x-pack/plugins/maps/public/classes/sources/client_file_source/index.js b/x-pack/plugins/maps/public/classes/sources/client_file_source/index.ts similarity index 96% rename from x-pack/plugins/maps/public/classes/sources/client_file_source/index.js rename to x-pack/plugins/maps/public/classes/sources/client_file_source/index.ts index 5c2a0afd31885..3f78511bc0747 100644 --- a/x-pack/plugins/maps/public/classes/sources/client_file_source/index.js +++ b/x-pack/plugins/maps/public/classes/sources/client_file_source/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore export { GeojsonFileSource } from './geojson_file_source'; export { uploadLayerWizardConfig } from './upload_layer_wizard'; diff --git a/x-pack/plugins/maps/public/connected_components/_index.scss b/x-pack/plugins/maps/public/connected_components/_index.scss index a961e652046a6..6de2a51590700 100644 --- a/x-pack/plugins/maps/public/connected_components/_index.scss +++ b/x-pack/plugins/maps/public/connected_components/_index.scss @@ -1,5 +1,5 @@ @import 'gis_map/gis_map'; -@import 'layer_addpanel/index'; +@import 'add_layer_panel/index'; @import 'layer_panel/index'; @import 'widget_overlay/index'; @import 'toolbar_overlay/index'; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/_index.scss b/x-pack/plugins/maps/public/connected_components/add_layer_panel/_index.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_addpanel/_index.scss rename to x-pack/plugins/maps/public/connected_components/add_layer_panel/_index.scss diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx new file mode 100644 index 0000000000000..75fb7a5bc4acc --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx @@ -0,0 +1,61 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiButtonEmpty, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { LayerWizardSelect } from './layer_wizard_select'; +import { LayerWizard, RenderWizardArguments } from '../../../classes/layers/layer_wizard_registry'; +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +type Props = RenderWizardArguments & { + layerWizard: LayerWizard | null; + onClear: () => void; + onWizardSelect: (layerWizard: LayerWizard) => void; +}; + +export const FlyoutBody = (props: Props) => { + function renderContent() { + if (!props.layerWizard) { + return ; + } + + const renderWizardArgs = { + previewLayer: props.previewLayer, + mapColors: props.mapColors, + isIndexingTriggered: props.isIndexingTriggered, + onRemove: props.onRemove, + onIndexReady: props.onIndexReady, + importSuccessHandler: props.importSuccessHandler, + importErrorHandler: props.importErrorHandler, + }; + + const backButton = props.isIndexingTriggered ? null : ( + + + + + + + ); + + return ( + + {backButton} + {props.layerWizard.renderWizard(renderWizardArgs)} + + ); + } + + return ( +
+
{renderContent()}
+
+ ); +}; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/index.ts similarity index 67% rename from x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js rename to x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/index.ts index bff235a7d27fc..c45937e70a805 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/index.ts @@ -5,27 +5,26 @@ */ import { connect } from 'react-redux'; -import { ImportEditor } from './view'; - -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; - +import { FlyoutBody } from './flyout_body'; import { INDEXING_STAGE } from '../../../reducers/ui'; import { updateIndexingStage } from '../../../actions/ui_actions'; import { getIndexingStage } from '../../../selectors/ui_selectors'; +import { MapStoreState } from '../../../reducers/store'; +import { getMapColors } from '../../../selectors/map_selectors'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { return { - inspectorAdapters: getInspectorAdapters(state), isIndexingTriggered: getIndexingStage(state) === INDEXING_STAGE.TRIGGERED, + mapColors: getMapColors(state), }; } const mapDispatchToProps = { - onIndexReady: indexReady => + onIndexReady: (indexReady: boolean) => indexReady ? updateIndexingStage(INDEXING_STAGE.READY) : updateIndexingStage(null), importSuccessHandler: () => updateIndexingStage(INDEXING_STAGE.SUCCESS), importErrorHandler: () => updateIndexingStage(INDEXING_STAGE.ERROR), }; -const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(ImportEditor); -export { connectedFlyOut as ImportEditor }; +const connected = connect(mapStateToProps, mapDispatchToProps)(FlyoutBody); +export { connected as FlyoutBody }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/layer_wizard_select.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/layer_wizard_select.tsx similarity index 94% rename from x-pack/plugins/maps/public/connected_components/layer_addpanel/layer_wizard_select.tsx rename to x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/layer_wizard_select.tsx index 80afe5c8f8a36..2b1bbfa81c743 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/layer_wizard_select.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/layer_wizard_select.tsx @@ -7,7 +7,7 @@ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; -import { getLayerWizards, LayerWizard } from '../../classes/layers/layer_wizard_registry'; +import { getLayerWizards, LayerWizard } from '../../../classes/layers/layer_wizard_registry'; interface Props { onSelect: (layerWizard: LayerWizard) => void; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts similarity index 68% rename from x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js rename to x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts index 757886a9b3a7d..f0563d1cbf2be 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts @@ -4,20 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AnyAction, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { FlyoutFooter } from './view'; import { getSelectedLayer } from '../../../selectors/map_selectors'; import { clearTransientLayerStateAndCloseFlyout } from '../../../actions/map_actions'; +import { MapStoreState } from '../../../reducers/store'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { const selectedLayer = getSelectedLayer(state); + const hasLayerSelected = !!selectedLayer; return { - hasLayerSelected: !!selectedLayer, - isLoading: selectedLayer && selectedLayer.isLayerLoading(), + hasLayerSelected, + isLoading: hasLayerSelected && selectedLayer!.isLayerLoading(), }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: Dispatch) { return { closeFlyout: () => dispatch(clearTransientLayerStateAndCloseFlyout()), }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx similarity index 87% rename from x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js rename to x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx index 7eb148a36abf1..6f4d25a9c6c3e 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx @@ -14,6 +14,16 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +interface Props { + onClick: () => void; + showNextButton: boolean; + disableNextButton: boolean; + nextButtonText: string; + closeFlyout: () => void; + hasLayerSelected: boolean; + isLoading: boolean; +} + export const FlyoutFooter = ({ onClick, showNextButton, @@ -22,7 +32,7 @@ export const FlyoutFooter = ({ closeFlyout, hasLayerSelected, isLoading, -}) => { +}: Props) => { const nextButton = showNextButton ? ( ) { return { - previewLayer: async layerDescriptor => { + previewLayer: async (layerDescriptor: LayerDescriptor) => { await dispatch(setSelectedLayer(null)); await dispatch(removeTransientLayer()); dispatch(addLayer(layerDescriptor)); @@ -54,7 +51,7 @@ function mapDispatchToProps(dispatch) { }; } -const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })( +const connected = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })( AddLayerPanel ); -export { connectedFlyOut as AddLayerPanel }; +export { connected as AddLayerPanel }; diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx new file mode 100644 index 0000000000000..d382a4085fe19 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx @@ -0,0 +1,156 @@ +/* + * 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 { EuiTitle, EuiFlyoutHeader } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FlyoutFooter } from './flyout_footer'; +import { FlyoutBody } from './flyout_body'; +import { LayerDescriptor } from '../../../common/descriptor_types'; +import { LayerWizard } from '../../classes/layers/layer_wizard_registry'; + +interface Props { + flyoutVisible: boolean; + isIndexingReady: boolean; + isIndexingSuccess: boolean; + isIndexingTriggered: boolean; + previewLayer: (layerDescriptor: LayerDescriptor) => void; + removeTransientLayer: () => void; + resetIndexing: () => void; + selectLayerAndAdd: () => void; + setIndexingTriggered: () => void; +} + +interface State { + importView: boolean; + isIndexingSource: boolean; + layerDescriptor: LayerDescriptor | null; + layerImportAddReady: boolean; + layerWizard: LayerWizard | null; +} + +export class AddLayerPanel extends Component { + private _isMounted: boolean = false; + + state = { + layerWizard: null, + layerDescriptor: null, // TODO get this from redux store instead of storing locally + isIndexingSource: false, + importView: false, + layerImportAddReady: false, + }; + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + if (!this.state.layerImportAddReady && this.props.isIndexingSuccess) { + this.setState({ layerImportAddReady: true }); + } + } + + _previewLayer = (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => { + if (!this._isMounted) { + return; + } + if (!layerDescriptor) { + this.setState({ + layerDescriptor: null, + isIndexingSource: false, + }); + this.props.removeTransientLayer(); + return; + } + + this.setState({ layerDescriptor, isIndexingSource: !!isIndexingSource }); + this.props.previewLayer(layerDescriptor); + }; + + _clearLayerData = ({ keepSourceType = false }: { keepSourceType: boolean }) => { + if (!this._isMounted) { + return; + } + + const newState: Partial = { + layerDescriptor: null, + isIndexingSource: false, + }; + if (!keepSourceType) { + newState.layerWizard = null; + newState.importView = false; + } + // @ts-ignore + this.setState(newState); + + this.props.removeTransientLayer(); + }; + + _onWizardSelect = (layerWizard: LayerWizard) => { + this.setState({ layerWizard, importView: !!layerWizard.isIndexingSource }); + }; + + _layerAddHandler = () => { + if (this.state.isIndexingSource && !this.props.isIndexingTriggered) { + this.props.setIndexingTriggered(); + } else { + this.props.selectLayerAndAdd(); + if (this.state.importView) { + this.setState({ + layerImportAddReady: false, + }); + this.props.resetIndexing(); + } + } + }; + + render() { + if (!this.props.flyoutVisible) { + return null; + } + + const panelDescription = + this.state.layerImportAddReady || !this.state.importView + ? i18n.translate('xpack.maps.addLayerPanel.addLayer', { + defaultMessage: 'Add layer', + }) + : i18n.translate('xpack.maps.addLayerPanel.importFile', { + defaultMessage: 'Import file', + }); + const isNextBtnEnabled = this.state.importView + ? this.props.isIndexingReady || this.props.isIndexingSuccess + : !!this.state.layerDescriptor; + + return ( + + + +

{panelDescription}

+
+
+ + this._clearLayerData({ keepSourceType: false })} + onRemove={() => this._clearLayerData({ keepSourceType: true })} + onWizardSelect={this._onWizardSelect} + previewLayer={this._previewLayer} + /> + + +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js index 0100db0393d06..621db4971b5d7 100644 --- a/x-pack/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js @@ -11,7 +11,7 @@ import { MBMapContainer } from '../map/mb'; import { WidgetOverlay } from '../widget_overlay'; import { ToolbarOverlay } from '../toolbar_overlay'; import { LayerPanel } from '../layer_panel'; -import { AddLayerPanel } from '../layer_addpanel'; +import { AddLayerPanel } from '../add_layer_panel'; import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js deleted file mode 100644 index 0dca2b8bd002c..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiPanel } from '@elastic/eui'; - -import { uploadLayerWizardConfig } from '../../../classes/sources/client_file_source'; - -export const ImportEditor = props => { - const editorProperties = getEditorProperties(props); - return ( - - {uploadLayerWizardConfig.renderWizard(editorProperties)} - - ); -}; - -function getEditorProperties({ - previewLayer, - mapColors, - onRemove, - isIndexingTriggered, - onIndexReady, - importSuccessHandler, - importErrorHandler, -}) { - return { - previewLayer, - mapColors, - onRemove, - importSuccessHandler, - importErrorHandler, - isIndexingTriggered, - onIndexReady, - }; -} diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js deleted file mode 100644 index 730e58a107aad..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js +++ /dev/null @@ -1,176 +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 { LayerWizardSelect } from './layer_wizard_select'; -import { FlyoutFooter } from './flyout_footer'; -import { ImportEditor } from './import_editor'; -import { EuiButtonEmpty, EuiPanel, EuiTitle, EuiFlyoutHeader, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class AddLayerPanel extends Component { - state = { - layerWizard: null, - layerDescriptor: null, // TODO get this from redux store instead of storing locally - isIndexingSource: false, - importView: false, - layerImportAddReady: false, - }; - - componentDidMount() { - this._isMounted = true; - } - - componentWillUnmount() { - this._isMounted = false; - } - - componentDidUpdate() { - if (!this.state.layerImportAddReady && this.props.isIndexingSuccess) { - this.setState({ layerImportAddReady: true }); - } - } - - _getPanelDescription() { - const { importView, layerImportAddReady } = this.state; - let panelDescription; - if (layerImportAddReady || !importView) { - panelDescription = i18n.translate('xpack.maps.addLayerPanel.addLayer', { - defaultMessage: 'Add layer', - }); - } else { - panelDescription = i18n.translate('xpack.maps.addLayerPanel.importFile', { - defaultMessage: 'Import file', - }); - } - return panelDescription; - } - - _previewLayer = async (layerDescriptor, isIndexingSource) => { - if (!this._isMounted) { - return; - } - if (!layerDescriptor) { - this.setState({ - layerDescriptor: null, - isIndexingSource: false, - }); - this.props.removeTransientLayer(); - return; - } - - this.setState({ layerDescriptor, isIndexingSource }); - this.props.previewLayer(layerDescriptor); - }; - - _clearLayerData = ({ keepSourceType = false }) => { - if (!this._isMounted) { - return; - } - - this.setState({ - layerDescriptor: null, - isIndexingSource: false, - ...(!keepSourceType ? { layerWizard: null, importView: false } : {}), - }); - this.props.removeTransientLayer(); - }; - - _onWizardSelect = layerWizard => { - this.setState({ layerWizard, importView: !!layerWizard.isIndexingSource }); - }; - - _layerAddHandler = () => { - if (this.state.isIndexingSource && !this.props.isIndexingTriggered) { - this.props.setIndexingTriggered(); - } else { - this.props.selectLayerAndAdd(); - if (this.state.importView) { - this.setState({ - layerImportAddReady: false, - }); - this.props.resetIndexing(); - } - } - }; - - _renderPanelBody() { - if (!this.state.layerWizard) { - return ; - } - - const backButton = this.props.isIndexingTriggered ? null : ( - - - - - - - ); - - if (this.state.importView) { - return ( - - {backButton} - this._clearLayerData({ keepSourceType: true })} - /> - - ); - } - - return ( - - {backButton} - - {this.state.layerWizard.renderWizard({ - previewLayer: this._previewLayer, - mapColors: this.props.mapColors, - })} - - - ); - } - - render() { - if (!this.props.flyoutVisible) { - return null; - } - - const panelDescription = this._getPanelDescription(); - const isNextBtnEnabled = this.state.importView - ? this.props.isIndexingReady || this.props.isIndexingSuccess - : !!this.state.layerDescriptor; - - return ( - - - -

{panelDescription}

-
-
- -
-
{this._renderPanelBody()}
-
- - -
- ); - } -} diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts index 77b5293dfaa09..e3a9596c9e374 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -28,3 +28,7 @@ export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer; export function getLayerList(state: MapStoreState): ILayer[]; export function getFittableLayers(state: MapStoreState): ILayer[]; + +export function getSelectedLayer(state: MapStoreState): ILayer | undefined; + +export function getMapColors(state: MapStoreState): string[]; diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 7de0a69fb0079..c50a00b2ec01a 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -8,6 +8,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; interface SetupDeps { features: FeaturesPluginSetupContract; @@ -47,6 +48,9 @@ export class MapsPlugin implements Plugin { }, }, }); + + core.savedObjects.registerType(mapsTelemetrySavedObjects); + core.savedObjects.registerType(mapSavedObjects); } start(core: CoreStart) {} } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.ts b/x-pack/plugins/maps/server/saved_objects/index.ts similarity index 70% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.ts rename to x-pack/plugins/maps/server/saved_objects/index.ts index 83ed9671238b1..c4b779183a2de 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.ts +++ b/x-pack/plugins/maps/server/saved_objects/index.ts @@ -3,6 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -export { AlertAdd } from './alert_add'; -export { AlertEdit } from './alert_edit'; +export { mapsTelemetrySavedObjects } from './maps_telemetry'; +export { mapSavedObjects } from './map'; diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts new file mode 100644 index 0000000000000..05f76c060ca94 --- /dev/null +++ b/x-pack/plugins/maps/server/saved_objects/map.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsType } from 'src/core/server'; +import { APP_ICON, createMapPath } from '../../common/constants'; +// @ts-ignore +import { migrations } from './migrations'; + +export const mapSavedObjects: SavedObjectsType = { + name: 'map', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + description: { type: 'text' }, + title: { type: 'text' }, + version: { type: 'integer' }, + bounds: { type: 'geo_shape' }, + mapStateJSON: { type: 'text' }, + layerListJSON: { type: 'text' }, + uiStateJSON: { type: 'text' }, + }, + }, + management: { + icon: APP_ICON, + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: createMapPath(obj.id), + uiCapabilitiesPath: 'maps.show', + }; + }, + }, + migrations: migrations.map, +}; diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts new file mode 100644 index 0000000000000..2512bf3094bcf --- /dev/null +++ b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts @@ -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 { SavedObjectsType } from 'src/core/server'; + +export const mapsTelemetrySavedObjects: SavedObjectsType = { + name: 'maps-telemetry', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + settings: { + properties: { + showMapVisualizationTypes: { type: 'boolean' }, + }, + }, + indexPatternsWithGeoFieldCount: { type: 'long' }, + indexPatternsWithGeoPointFieldCount: { type: 'long' }, + indexPatternsWithGeoShapeFieldCount: { type: 'long' }, + mapsTotalCount: { type: 'long' }, + timeCaptured: { type: 'date' }, + attributesPerMap: { + properties: { + dataSourcesCount: { + properties: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'long' }, + }, + }, + layersCount: { + properties: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'long' }, + }, + }, + layerTypesCount: { dynamic: 'true', properties: {} }, + emsVectorLayersCount: { dynamic: 'true', properties: {} }, + }, + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/plugins/maps/server/saved_objects/migrations.js similarity index 62% rename from x-pack/legacy/plugins/maps/migrations.js rename to x-pack/plugins/maps/server/saved_objects/migrations.js index d3666025082b7..13b38353d6807 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/plugins/maps/server/saved_objects/migrations.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { extractReferences } from '../../../plugins/maps/common/migrations/references'; -import { emsRasterTileToEmsVectorTile } from '../../../plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile'; -import { topHitsTimeToSort } from '../../../plugins/maps/common/migrations/top_hits_time_to_sort'; -import { moveApplyGlobalQueryToSources } from '../../../plugins/maps/common/migrations/move_apply_global_query'; -import { addFieldMetaOptions } from '../../../plugins/maps/common/migrations/add_field_meta_options'; -import { migrateSymbolStyleDescriptor } from '../../../plugins/maps/common/migrations/migrate_symbol_style_descriptor'; -import { migrateUseTopHitsToScalingType } from '../../../plugins/maps/common/migrations/scaling_type'; -import { migrateJoinAggKey } from '../../../plugins/maps/common/migrations/join_agg_key'; +import { extractReferences } from '../../common/migrations/references'; +import { emsRasterTileToEmsVectorTile } from '../../common/migrations/ems_raster_tile_to_ems_vector_tile'; +import { topHitsTimeToSort } from '../../common/migrations/top_hits_time_to_sort'; +import { moveApplyGlobalQueryToSources } from '../../common/migrations/move_apply_global_query'; +import { addFieldMetaOptions } from '../../common/migrations/add_field_meta_options'; +import { migrateSymbolStyleDescriptor } from '../../common/migrations/migrate_symbol_style_descriptor'; +import { migrateUseTopHitsToScalingType } from '../../common/migrations/scaling_type'; +import { migrateJoinAggKey } from '../../common/migrations/join_agg_key'; export const migrations = { map: { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 58bc75bd7309b..01cce153ce494 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -17,9 +17,14 @@ import { EuiSpacer, } from '@elastic/eui'; -import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; +import { + getAnalysisType, + DataFrameAnalyticsId, + useRefreshAnalyticsList, + ANALYSIS_CONFIG_TYPE, +} from '../../../../common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; -import { getTaskStateBadge } from './columns'; +import { getTaskStateBadge, getJobTypeBadge } from './columns'; import { DataFrameAnalyticsListColumn, @@ -154,7 +159,7 @@ export const DataFrameAnalyticsList: FC = ({ clauses.forEach(c => { // the search term could be negated with a minus, e.g. -bananas const bool = c.match === 'must'; - let ts = []; + let ts: DataFrameAnalyticsListRow[]; if (c.type === 'term') { // filter term based clauses, e.g. bananas @@ -174,8 +179,14 @@ export const DataFrameAnalyticsList: FC = ({ } else { // filter other clauses, i.e. the mode and status filters if (Array.isArray(c.value)) { - // the status value is an array of string(s) e.g. ['failed', 'stopped'] - ts = analytics.filter(d => (c.value as string).includes(d.stats.state)); + if (c.field === 'job_type') { + ts = analytics.filter(d => + (c.value as string).includes(getAnalysisType(d.config.analysis)) + ); + } else { + // the status value is an array of string(s) e.g. ['failed', 'stopped'] + ts = analytics.filter(d => (c.value as string).includes(d.stats.state)); + } } else { ts = analytics.filter(d => d.mode === c.value); } @@ -291,6 +302,19 @@ export const DataFrameAnalyticsList: FC = ({ incremental: true, }, filters: [ + { + type: 'field_value_selection', + field: 'job_type', + name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: Object.values(ANALYSIS_CONFIG_TYPE).map(val => ({ + value: val, + name: val, + view: getJobTypeBadge(val), + })), + }, { type: 'field_value_selection', field: 'state.state', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 907297cf69bfc..194d59faccf3f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -64,6 +64,12 @@ export const getTaskStateBadge = ( ); }; +export const getJobTypeBadge = (jobType: string) => ( + + {jobType} + +); + export const progressColumn = { name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', { defaultMessage: 'Progress per Step', @@ -230,7 +236,7 @@ export const getColumns = ( sortable: (item: DataFrameAnalyticsListRow) => getAnalysisType(item.config.analysis), truncateText: true, render(item: DataFrameAnalyticsListRow) { - return {getAnalysisType(item.config.analysis)}; + return getJobTypeBadge(getAnalysisType(item.config.analysis)); }, width: '150px', 'data-test-subj': 'mlAnalyticsTableColumnType', diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index c5f02863ba8a1..0ed6917854dc4 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -69,13 +69,13 @@ export function getAlertType(): AlertTypeModel { id: '.index-threshold', name: 'Index threshold', iconClass: 'alert', - alertParamsExpression: IndexThresholdAlertTypeExpression, + alertParamsExpression: lazy(() => import('./index_threshold_expression')), validate: validateAlertType, }; } ``` -alertParamsExpression form represented as an expression using `EuiExpression` components: +alertParamsExpression should be a lazy loaded React component extending an expression using `EuiExpression` components: ![Index Threshold Alert expression form](https://i.imgur.com/Ysk1ljY.png) ``` @@ -171,6 +171,7 @@ export const alertReducer = (state: any, action: AlertReducerAction) => { ``` +The Expression component should be lazy loaded which means it'll have to be the default export in `index_threshold_expression.ts`: ``` export const IndexThresholdAlertTypeExpression: React.FunctionComponent = ({ @@ -224,6 +225,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent ); }; + +// Export as default in order to support lazy loading +export {IndexThresholdAlertTypeExpression as default}; ``` Index Threshold Alert form with validation: @@ -237,7 +241,9 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop name: string; iconClass: string; validate: (alertParams: any) => ValidationResult; - alertParamsExpression: React.FunctionComponent; + alertParamsExpression: React.LazyExoticComponent< + ComponentType> + >; defaultActionMessage?: string; ``` |Property|Description| @@ -246,7 +252,7 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop |name|Name of the alert type that will be displayed on the select card in the UI.| |iconClass|Icon of the alert type that will be displayed on the select card in the UI.| |validate|Validation function for the alert params.| -|alertParamsExpression|React functional component for building UI of the current alert type params.| +|alertParamsExpression| A lazy loaded React component for building UI of the current alert type params.| |defaultActionMessage|Optional property for providing default message for all added actions with `message` property.| IMPORTANT: The current UI supports a single action group only. @@ -295,8 +301,8 @@ Below is a list of steps that should be done to build and register a new alert t 1. At any suitable place in Kibana, create a file, which will expose an object implementing interface [AlertTypeModel](https://github.com/elastic/kibana/blob/55b7905fb5265b73806006e7265739545d7521d0/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts#L83). Example: ``` +import { lazy } from 'react'; import { AlertTypeModel } from '../../../../types'; -import { ExampleExpression } from './expression'; import { validateExampleAlertType } from './validation'; export function getAlertType(): AlertTypeModel { @@ -304,7 +310,7 @@ export function getAlertType(): AlertTypeModel { id: 'example', name: 'Example Alert Type', iconClass: 'bell', - alertParamsExpression: ExampleExpression, + alertParamsExpression: lazy(() => import('./expression')), validate: validateExampleAlertType, defaultActionMessage: 'Alert [{{ctx.metadata.name}}] has exceeded the threshold', }; @@ -361,6 +367,9 @@ export const ExampleExpression: React.FunctionComponent = ({ ); }; +// Export as default in order to support lazy loading +export {ExampleExpression as default}; + ``` This alert type form becomes available, when the card of `Example Alert Type` is selected. Each expression word here is `EuiExpression` component and implements the basic aggregation, grouping and comparison methods. @@ -1017,7 +1026,7 @@ Below is a list of steps that should be done to build and register a new action 1. At any suitable place in Kibana, create a file, which will expose an object implementing interface [ActionTypeModel]: ``` -import React, { Fragment } from 'react'; +import React, { Fragment, lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { ActionTypeModel, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 63860e062c8da..ebd9294ce1e6d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -3,8 +3,8 @@ * 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, { lazy, Suspense } from 'react'; -import { Switch, Route, Redirect, HashRouter, RouteComponentProps } from 'react-router-dom'; +import React, { lazy } from 'react'; +import { Switch, Route, Redirect, HashRouter } from 'react-router-dom'; import { ChromeStart, DocLinksStart, @@ -15,7 +15,6 @@ import { ChromeBreadcrumb, CoreStart, } from 'kibana/public'; -import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { BASE_PATH, Section, routeToAlertDetails } from './constants'; import { AppContextProvider, useAppDependencies } from './app_context'; import { hasShowAlertsCapability } from './lib/capabilities'; @@ -24,6 +23,7 @@ import { TypeRegistry } from './type_registry'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerting/public'; +import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; const TriggersActionsUIHome = lazy(async () => import('./home')); const AlertDetailsRoute = lazy(() => @@ -68,30 +68,15 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) = {canShowAlerts && ( - + )} ); }; - -function suspendedRouteComponent( - RouteComponent: React.ComponentType> -) { - return (props: RouteComponentProps) => ( - - - - - - } - > - - - ); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 43955db97f295..7803ed1ac3a7f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -42,6 +42,7 @@ import { } from '../../../../common'; import { builtInAggregationTypes } from '../../../../common/constants'; import { IndexThresholdAlertParams } from './types'; +import { AlertTypeParamsExpressionProps } from '../../../../types'; import { AlertsContextValue } from '../../../context/alerts_context'; import './expression.scss'; @@ -66,23 +67,10 @@ const expressionFieldsWithValidation = [ 'timeWindowSize', ]; -interface IndexThresholdProps { - alertParams: IndexThresholdAlertParams; - alertInterval: string; - setAlertParams: (property: string, value: any) => void; - setAlertProperty: (key: string, value: any) => void; - errors: { [key: string]: string[] }; - alertsContext: AlertsContextValue; -} - -export const IndexThresholdAlertTypeExpression: React.FunctionComponent = ({ - alertParams, - alertInterval, - setAlertParams, - setAlertProperty, - errors, - alertsContext, -}) => { +export const IndexThresholdAlertTypeExpression: React.FunctionComponent> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { const { index, timeField, @@ -476,3 +464,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent ); }; + +// eslint-disable-next-line import/no-default-export +export { IndexThresholdAlertTypeExpression as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts index 983f759214b6b..42747b9e85e2e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts @@ -3,16 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { lazy } from 'react'; + import { AlertTypeModel } from '../../../../types'; -import { IndexThresholdAlertTypeExpression } from './expression'; import { validateExpression } from './validation'; +import { IndexThresholdAlertParams } from './types'; +import { AlertsContextValue } from '../../../context/alerts_context'; -export function getAlertType(): AlertTypeModel { +export function getAlertType(): AlertTypeModel { return { id: '.index-threshold', name: 'Index threshold', iconClass: 'alert', - alertParamsExpression: IndexThresholdAlertTypeExpression, + alertParamsExpression: lazy(() => import('./expression')), validate: validateExpression, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/suspended_component_with_props.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/suspended_component_with_props.tsx new file mode 100644 index 0000000000000..563353793f991 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/suspended_component_with_props.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Suspense } from 'react'; +import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiLoadingSpinnerSize } from '@elastic/eui/src/components/loading/loading_spinner'; + +export function suspendedComponentWithProps( + ComponentToSuspend: React.ComponentType, + size?: EuiLoadingSpinnerSize +) { + return (props: T) => ( + + + + + + } + > + + + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index cdc187bc6f3ba..931fde430c601 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -10,7 +10,7 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; -import { ActionForm } from './action_form'; +import ActionForm from './action_form'; jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), loadActionTypes: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index f3e955c973309..5af56f410ad50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -713,3 +713,6 @@ export const ActionForm = ({ ); }; + +// eslint-disable-next-line import/no-default-export +export { ActionForm as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index ac6c0e2749776..4f5007949f8b1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { ConnectorAddFlyout } from './connector_add_flyout'; +import ConnectorAddFlyout from './connector_add_flyout'; import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index c9844f4e10864..adee2e09a56fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -319,3 +319,6 @@ const UpgradeYourLicenseCallOut = ({ http }: { http: HttpSetup }) => ( ); + +// eslint-disable-next-line import/no-default-export +export { ConnectorAddFlyout as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 976ec146181c2..e4a9e6e74173e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -9,7 +9,7 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { ConnectorEditFlyout } from './connector_edit_flyout'; +import ConnectorEditFlyout from './connector_edit_flyout'; import { AppContextProvider } from '../../app_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 4a0effcbd6825..6ea78f60c52ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -254,3 +254,6 @@ export const ConnectorEditFlyout = ({ ); }; + +// eslint-disable-next-line import/no-default-export +export { ConnectorEditFlyout as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts index 52ee1efbdaf9f..e0065c143a1a2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts @@ -4,6 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ConnectorAddFlyout } from './connector_add_flyout'; -export { ConnectorEditFlyout } from './connector_edit_flyout'; -export { ActionForm } from './action_form'; +import { lazy } from 'react'; +import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; + +export const ConnectorAddFlyout = suspendedComponentWithProps( + lazy(() => import('./connector_add_flyout')) +); +export const ConnectorEditFlyout = suspendedComponentWithProps( + lazy(() => import('./connector_edit_flyout')) +); +export const ActionForm = suspendedComponentWithProps(lazy(() => import('./action_form'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 01d21e954bbf3..12b6f99319596 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -202,11 +202,12 @@ describe('actions_connectors_list component with items', () => { expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2); }); - test('if select item for edit should render ConnectorEditFlyout', () => { - wrapper + test('if select item for edit should render ConnectorEditFlyout', async () => { + await wrapper .find('[data-test-subj="edit1"]') .first() .simulate('click'); + expect(wrapper.find('ConnectorEditFlyout')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 9267a154efaa0..64a7aa9ffa8b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -22,7 +22,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAppDependencies } from '../../../app_context'; import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api'; -import { ConnectorAddFlyout, ConnectorEditFlyout } from '../../action_connector_form'; +import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; +import ConnectorEditFlyout from '../../action_connector_form/connector_edit_flyout'; + import { hasDeleteActionsCapability, hasSaveActionsCapability } from '../../../lib/capabilities'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 3d6493a5131e5..bebbcdda10a00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormLabel } from '@elastic/eui'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { AlertAdd } from './alert_add'; +import AlertAdd from './alert_add'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; import { AlertsContextProvider, useAlertsContext } from '../../context/alerts_context'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 651f2cdba34af..004ad97083fe4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -219,3 +219,6 @@ const parseErrors: (errors: IErrorObject) => boolean = errors => if (isObject(errorList)) return parseErrors(errorList as IErrorObject); return errorList.length >= 1; }); + +// eslint-disable-next-line import/no-default-export +export { AlertAdd as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 4d8801d8b7484..39112a1509580 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -12,7 +12,7 @@ import { ValidationResult } from '../../../types'; import { AlertsContextProvider } from '../../context/alerts_context'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { ReactWrapper } from 'enzyme'; -import { AlertEdit } from './alert_edit'; +import AlertEdit from './alert_edit'; import { AppContextProvider } from '../../app_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 747464d2212f4..fc1a3778bc5b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -201,3 +201,6 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { ); }; + +// eslint-disable-next-line import/no-default-export +export { AlertEdit as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 3b7283e69e019..e956c8ecc4f3b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -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 React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, Suspense } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -23,6 +23,7 @@ import { EuiIconTip, EuiButtonIcon, EuiHorizontalRule, + EuiLoadingSpinner, } from '@elastic/eui'; import { some, filter, map, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -36,7 +37,7 @@ import { AlertReducerAction } from './alert_reducer'; import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; -import { ActionForm } from '../action_connector_form/action_form'; +import { ActionForm } from '../action_connector_form'; export function validateBaseProperties(alertObject: Alert) { const validationResult = { errors: {} }; @@ -222,14 +223,24 @@ export const AlertForm = ({ ) : null} {AlertParamsExpressionComponent ? ( - + + + + + + } + > + + ) : null} {defaultActionGroupId ? ( import('./alert_add'))); +export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx new file mode 100644 index 0000000000000..677ee139271c0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { suspendedComponentWithProps } from '../lib/suspended_component_with_props'; + +export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_add'))); +export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_edit'))); + +export const ConnectorAddFlyout = suspendedComponentWithProps( + lazy(() => import('./action_connector_form/connector_add_flyout')) +); +export const ConnectorEditFlyout = suspendedComponentWithProps( + lazy(() => import('./action_connector_form/connector_edit_flyout')) +); +export const ActionForm = suspendedComponentWithProps( + lazy(() => import('./action_connector_form/action_form')) +); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx index defad2b801718..5405d96bb1dce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx @@ -20,11 +20,12 @@ import { getTimeUnitLabel } from '../lib/get_time_unit_label'; import { TIME_UNITS } from '../../application/constants'; import { getTimeOptions } from '../lib/get_time_options'; import { ClosablePopoverTitle } from './components'; +import { IErrorObject } from '../../types'; interface ForLastExpressionProps { timeWindowSize?: number; timeWindowUnit?: string; - errors: { [key: string]: string[] }; + errors: IErrorObject; onChangeWindowSize: (selectedWindowSize: number | undefined) => void; onChangeWindowUnit: (selectedWindowUnit: string) => void; popupPosition?: diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx index 619d85d99719b..33ca98de4c08b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -19,10 +19,11 @@ import { import { builtInGroupByTypes } from '../constants'; import { GroupByType } from '../types'; import { ClosablePopoverTitle } from './components'; +import { IErrorObject } from '../../types'; interface GroupByExpressionProps { groupBy: string; - errors: { [key: string]: string[] }; + errors: IErrorObject; onChangeSelectedTermSize: (selectedTermSize?: number) => void; onChangeSelectedTermField: (selectedTermField?: string) => void; onChangeSelectedGroupBy: (selectedGroupBy?: string) => void; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 96645e856e418..a72d8815c95b4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -11,7 +11,13 @@ export { AlertsContextProvider } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; export { ActionForm } from './application/sections/action_connector_form'; -export { AlertAction, Alert, AlertTypeModel, ActionType } from './types'; +export { + AlertAction, + Alert, + AlertTypeModel, + AlertTypeParamsExpressionProps, + ActionType, +} from './types'; export { ConnectorAddFlyout, ConnectorEditFlyout, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index cc511434267cc..e9cfd5b33db23 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -110,12 +110,28 @@ export interface AlertTableItem extends Alert { tagsText: string; } -export interface AlertTypeModel { +export interface AlertTypeParamsExpressionProps< + AlertParamsType = unknown, + AlertsContextValue = unknown +> { + alertParams: AlertParamsType; + alertInterval: string; + setAlertParams: (property: string, value: any) => void; + setAlertProperty: (key: string, value: any) => void; + errors: IErrorObject; + alertsContext: AlertsContextValue; +} + +export interface AlertTypeModel { id: string; name: string | JSX.Element; iconClass: string; - validate: (alertParams: any) => ValidationResult; - alertParamsExpression: React.FunctionComponent; + validate: (alertParams: AlertParamsType) => ValidationResult; + alertParamsExpression: + | React.FunctionComponent + | React.LazyExoticComponent< + ComponentType> + >; defaultActionMessage?: string; } diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index c6a7eb261d8fd..b589bd64591fc 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; -import { - LegacyCoreStart, - AppMountParameters, - DEFAULT_APP_CATEGORIES, -} from '../../../../../src/core/public'; +import { AppMountParameters, DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { UMFrontendLibs } from '../lib/lib'; import { PLUGIN } from '../../common/constants'; import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; @@ -17,11 +13,6 @@ import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -export interface StartObject { - core: LegacyCoreStart; - plugins: any; -} - export interface ClientPluginsSetup { data: DataPublicPluginSetup; home: HomePublicPluginSetup; diff --git a/x-pack/plugins/uptime/public/components/overview/overview_container.tsx b/x-pack/plugins/uptime/public/components/overview/overview_container.tsx index 320536bc63b3c..7fd71f3ac89be 100644 --- a/x-pack/plugins/uptime/public/components/overview/overview_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/overview_container.tsx @@ -6,16 +6,11 @@ import { useDispatch, useSelector } from 'react-redux'; import React, { useCallback } from 'react'; -import { DataPublicPluginSetup } from 'src/plugins/data/public'; import { OverviewPageComponent } from '../../pages/overview'; import { selectIndexPattern } from '../../state/selectors'; import { setEsKueryString } from '../../state/actions'; -export interface OverviewPageProps { - autocomplete: DataPublicPluginSetup['autocomplete']; -} - -export const OverviewPage: React.FC = props => { +export const OverviewPage: React.FC = props => { const dispatch = useDispatch(); const setEsKueryFilters = useCallback( (esFilters: string) => dispatch(setEsKueryString(esFilters)), diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 66e61fbf73b64..65827867da5ee 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -63,7 +63,9 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({ id: CLIENT_ALERT_TYPES.MONITOR_STATUS, name: , iconClass: 'uptimeApp', - alertParamsExpression: params => , + alertParamsExpression: (params: any) => ( + + ), validate, defaultActionMessage, }); diff --git a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap index 71b3fb5c7146a..791bb4a57ae52 100644 --- a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap +++ b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap @@ -51,15 +51,7 @@ exports[`MonitorPage shallow renders expected elements for valid props 1`] = ` } } > - { title: 'heartbeat-8*', }; - const autocomplete = { - getQuerySuggestions: jest.fn(), - hasQuerySuggestions: () => true, - getValueSuggestions: jest.fn(), - addQuerySuggestionProvider: jest.fn(), - }; - it('shallow renders expected elements for valid props', () => { expect( shallowWithRouter( - + ) ).toMatchSnapshot(); }); diff --git a/x-pack/plugins/uptime/public/pages/certificates.tsx b/x-pack/plugins/uptime/public/pages/certificates.tsx index a2b37657cf3fe..517252dcd1969 100644 --- a/x-pack/plugins/uptime/public/pages/certificates.tsx +++ b/x-pack/plugins/uptime/public/pages/certificates.tsx @@ -23,7 +23,6 @@ import { OVERVIEW_ROUTE, SETTINGS_ROUTE, CLIENT_ALERT_TYPES } from '../../common import { getDynamicSettings } from '../state/actions/dynamic_settings'; import { UptimeRefreshContext } from '../contexts'; import * as labels from './translations'; -import { UptimePage, useUptimeTelemetry } from '../hooks'; import { certificatesSelector, getCertificatesAction } from '../state/certificates/certificates'; import { CertificateList, CertificateSearch, CertSort } from '../components/certificates'; import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers'; @@ -39,8 +38,6 @@ const getPageSizeValue = () => { }; export const CertificatesPage: React.FC = () => { - useUptimeTelemetry(UptimePage.Certificates); - useTrackPageview({ app: 'uptime', path: 'certificates' }); useTrackPageview({ app: 'uptime', path: 'certificates', delay: 15000 }); diff --git a/x-pack/plugins/uptime/public/pages/monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor.tsx index fc796e679a2f6..129b673f9e102 100644 --- a/x-pack/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.tsx @@ -11,7 +11,7 @@ import { monitorStatusSelector } from '../state/selectors'; import { PageHeader } from './page_header'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { useTrackPageview } from '../../../observability/public'; -import { useMonitorId, useUptimeTelemetry, UptimePage } from '../hooks'; +import { useMonitorId } from '../hooks'; import { MonitorCharts } from '../components/monitor'; import { MonitorStatusDetails, PingList } from '../components/monitor'; import { getDynamicSettings } from '../state/actions/dynamic_settings'; @@ -27,8 +27,6 @@ export const MonitorPage: React.FC = () => { const selectedMonitor = useSelector(monitorStatusSelector); - useUptimeTelemetry(UptimePage.Monitor); - useTrackPageview({ app: 'uptime', path: 'monitor' }); useTrackPageview({ app: 'uptime', path: 'monitor', delay: 15000 }); diff --git a/x-pack/plugins/uptime/public/pages/overview.tsx b/x-pack/plugins/uptime/public/pages/overview.tsx index 65f64aa7352a9..639f363e6f9b1 100644 --- a/x-pack/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useEffect } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { useUptimeTelemetry, UptimePage, useGetUrlParams } from '../hooks'; +import { useGetUrlParams } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { PageHeader } from './page_header'; import { IIndexPattern } from '../../../../../src/plugins/data/public'; @@ -18,9 +18,9 @@ import { useTrackPageview } from '../../../observability/public'; import { MonitorList } from '../components/overview/monitor_list/monitor_list_container'; import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview'; import { StatusPanel } from '../components/overview/status_panel'; -import { OverviewPageProps } from '../components/overview/overview_container'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -interface Props extends OverviewPageProps { +interface Props { indexPattern: IIndexPattern | null; setEsKueryFilters: (esFilters: string) => void; } @@ -34,11 +34,15 @@ const EuiFlexItemStyled = styled(EuiFlexItem)` } `; -export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFilters }: Props) => { +export const OverviewPageComponent = React.memo(({ indexPattern, setEsKueryFilters }: Props) => { const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); const { search, filters: urlFilters } = params; - useUptimeTelemetry(UptimePage.Overview); + const { + services: { + data: { autocomplete }, + }, + } = useKibana(); useTrackPageview({ app: 'uptime', path: 'overview' }); useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 }); @@ -57,6 +61,7 @@ export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFi }); useBreadcrumbs([]); // No extra breadcrumbs on overview + return ( <> @@ -83,4 +88,4 @@ export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFi ); -}; +}); diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx index d018567ae1104..b617e81bad88a 100644 --- a/x-pack/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -25,7 +25,6 @@ import { DynamicSettings } from '../../common/runtime_types'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { OVERVIEW_ROUTE } from '../../common/constants'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { UptimePage, useUptimeTelemetry } from '../hooks'; import { IndicesForm } from '../components/settings/indices_form'; import { CertificateExpirationForm, @@ -75,13 +74,11 @@ const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldEr return null; }; -export const SettingsPage = () => { +export const SettingsPage: React.FC = () => { const dss = useSelector(selectDynamicSettings); useBreadcrumbs([{ text: Translations.settings.breadcrumbText }]); - useUptimeTelemetry(UptimePage.Settings); - const dispatch = useDispatch(); useEffect(() => { diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index ca97858998df7..455d5070128f5 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { DataPublicPluginSetup } from '../../../../src/plugins/data/public'; import { OverviewPage } from './components/overview/overview_container'; import { CERTIFICATES_ROUTE, @@ -16,33 +15,73 @@ import { } from '../common/constants'; import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; import { CertificatesPage } from './pages/certificates'; +import { UptimePage, useUptimeTelemetry } from './hooks'; -interface RouterProps { - autocomplete: DataPublicPluginSetup['autocomplete']; +interface RouteProps { + path: string; + component: React.FC; + dataTestSubj: string; + title: string; + telemetryId: UptimePage; } -export const PageRouter: FC = ({ autocomplete }) => ( - - -
- -
-
- -
- -
-
- -
- -
-
- -
- -
-
- -
-); +const baseTitle = 'Uptime - Kibana'; + +const Routes: RouteProps[] = [ + { + title: `Monitor | ${baseTitle}`, + path: MONITOR_ROUTE, + component: MonitorPage, + dataTestSubj: 'uptimeMonitorPage', + telemetryId: UptimePage.Monitor, + }, + { + title: `Settings | ${baseTitle}`, + path: SETTINGS_ROUTE, + component: SettingsPage, + dataTestSubj: 'uptimeSettingsPage', + telemetryId: UptimePage.Settings, + }, + { + title: `Certificates | ${baseTitle}`, + path: CERTIFICATES_ROUTE, + component: CertificatesPage, + dataTestSubj: 'uptimeCertificatesPage', + telemetryId: UptimePage.Certificates, + }, + { + title: baseTitle, + path: OVERVIEW_ROUTE, + component: OverviewPage, + dataTestSubj: 'uptimeOverviewPage', + telemetryId: UptimePage.Overview, + }, +]; + +const RouteInit: React.FC> = ({ + path, + title, + telemetryId, +}) => { + useUptimeTelemetry(telemetryId); + useEffect(() => { + document.title = title; + }, [path, title]); + return null; +}; + +export const PageRouter: FC = () => { + return ( + + {Routes.map(({ title, path, component: RouteComponent, dataTestSubj, telemetryId }) => ( + +
+ + +
+
+ ))} + +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/uptime_app.tsx b/x-pack/plugins/uptime/public/uptime_app.tsx index 836d942d92165..2891a15510f31 100644 --- a/x-pack/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/uptime_app.tsx @@ -106,7 +106,7 @@ const Application = (props: UptimeAppProps) => {
- +
diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index efe1f85905970..3ec7776f848af 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -21,6 +21,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), + require.resolve('../test/detection_engine_api_integration/basic/config.ts'), require.resolve('../test/plugin_api_integration/config.ts'), require.resolve('../test/kerberos_api_integration/config.ts'), require.resolve('../test/kerberos_api_integration/anonymous_access.config.ts'), diff --git a/x-pack/test/detection_engine_api_integration/basic/config.ts b/x-pack/test/detection_engine_api_integration/basic/config.ts new file mode 100644 index 0000000000000..f9c248ec3d56f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/config.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('basic', { + disabledPlugins: [], + license: 'basic', + ssl: true, +}); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts new file mode 100644 index 0000000000000..d740445ff9275 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('add_prepackaged_rules', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before adding prepackaged rules', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body).to.eql({ + message: + 'Pre-packaged rules cannot be installed until the signals index is created: .siem-signals-default', + status_code: 400, + }); + }); + }); + + describe('creating prepackaged rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should contain two output keys of rules_installed and rules_updated', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql(['rules_installed', 'rules_updated']); + }); + + it('should create the prepackaged rules and return a count greater than zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_installed).to.be.greaterThan(0); + }); + + it('should create the prepackaged rules that the rules_updated is of size zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_updated).to.eql(0); + }); + + it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_installed).to.eql(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts new file mode 100644 index 0000000000000..593f04c9bb736 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleMlRule, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('create_rules', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('creating rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should create a single rule without an input index', async () => { + const { index, ...payload } = getSimpleRule(); + const { index: _index, ...expected } = getSimpleRuleOutput(); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(payload) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(expected); + }); + + it('should create a single rule without a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should give a 403 when trying to create a single Machine Learning rule since the license is basic', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleMlRule()) + .expect(403); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql({ + message: 'Your license does not support machine learning. Please upgrade your license.', + status_code: 403, + }); + }); + + it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(409); + + expect(body).to.eql({ + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules_bulk.ts new file mode 100644 index 0000000000000..079792248b385 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules_bulk.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('create_rules_bulk', () => { + describe('validation errors', () => { + it('should give a 200 even if the index does not exist as all bulks return a 200 but have an error of 409 bad request in the body', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); + }); + + describe('creating rules in bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should create a single rule without a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRuleWithoutRuleId()]) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule(), getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ]); + }); + + it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id that already exists', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'foo') + .send([getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts new file mode 100644 index 0000000000000..c247dc7514f2b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('delete_rules', () => { + describe('deleting rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // create a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // delete the rule by its rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule where the rule_id is auto-generated + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its auto-generated rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=${bodyWithCreatedRule.rule_id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its auto-generated id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=${bodyWithCreatedRule.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'id: "fake_id" not found', + status_code: 404, + }); + }); + + it('should return an error if the rule_id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'rule_id: "fake_id" not found', + status_code: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts new file mode 100644 index 0000000000000..0945233115c6f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts @@ -0,0 +1,281 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('delete_rules_bulk', () => { + describe('deleting rules bulk using DELETE', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // add a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete the rule in bulk + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1' }]) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule without a rule_id + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: bodyWithCreatedRule.rule_id }]) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }]) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should return an error if the id does not exist when trying to delete an id', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + id: 'fake_id', + }, + ]); + }); + + it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => { + // add the rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + getSimpleRuleOutputWithoutRuleId(), + { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + }); + + // This is a repeat of the tests above but just using POST instead of DELETE + describe('deleting rules bulk using POST', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // add a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'foo') + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete the rule in bulk + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1' }]) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule without a rule_id + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its rule_id + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: bodyWithCreatedRule.rule_id }]) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its id + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }]) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should return an error if the id does not exist when trying to delete an id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + id: 'fake_id', + }, + ]); + }); + + it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => { + // add the rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + getSimpleRuleOutputWithoutRuleId(), + { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts new file mode 100644 index 0000000000000..05bb508e7f51d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + binaryToString, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('export_rules', () => { + describe('exporting rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should set the response content types to be expected', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .expect(200) + .expect('Content-Type', 'application/ndjson') + .expect('Content-Disposition', 'attachment; filename="export.ndjson"'); + }); + + it('should export a single rule with a rule_id', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .expect(200) + .parse(binaryToString); + + const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[0]); + const bodyToTest = removeServerGeneratedProperties(bodySplitAndParsed); + + expect(bodyToTest).to.eql(getSimpleRuleOutput()); + }); + + it('should export a exported count with a single rule_id', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .expect(200) + .parse(binaryToString); + + const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]); + + expect(bodySplitAndParsed).to.eql({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + }); + }); + + it('should export exactly two rules given two rules', async () => { + // post rule 1 + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // post rule 2 + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .expect(200) + .parse(binaryToString); + + const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); + const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); + const firstRule = removeServerGeneratedProperties(firstRuleParsed); + const secondRule = removeServerGeneratedProperties(secondRuleParsed); + + expect([firstRule, secondRule]).to.eql([ + getSimpleRuleOutput('rule-2'), + getSimpleRuleOutput('rule-1'), + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_rules.ts new file mode 100644 index 0000000000000..6a4b6035c318a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/find_rules.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getComplexRule, + getComplexRuleOutput, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('find_rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should return an empty find body correctly if no rules are loaded', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + data: [], + page: 1, + perPage: 20, + total: 0, + }); + }); + + it('should return a single rule when a single rule is loaded from a find with defaults added', async () => { + // add a single rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // query the single rule from _find + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getSimpleRuleOutput()], + page: 1, + perPage: 20, + total: 1, + }); + }); + + it('should return a single rule when a single rule is loaded from a find with everything for the rule added', async () => { + // add a single rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getComplexRule()) + .expect(200); + + // query and expect that we get back one record in the find + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getComplexRuleOutput()], + page: 1, + perPage: 20, + total: 1, + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts new file mode 100644 index 0000000000000..3b9bac3ec6721 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + deleteAllRulesStatuses, + getSimpleRule, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('find_statuses', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllRulesStatuses(es); + }); + + it('should return an empty find statuses body correctly if no statuses are loaded', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [] }) + .expect(200); + + expect(body).to.eql({}); + }); + + it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { + // add a single rule + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await new Promise(resolve => setTimeout(resolve, 5000)); + + // query the single rule from _find + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + + // expected result for status should be 'going to run' or 'succeeded + expect(['succeeded', 'going to run']).to.contain(body[resBody.id].current_status.status); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts new file mode 100644 index 0000000000000..601876da717f6 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/get_prepackaged_rules_status.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + DETECTION_ENGINE_PREPACKAGED_URL, + DETECTION_ENGINE_RULES_URL, +} from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('get_prepackaged_rules_status', () => { + describe('getting prepackaged rules status', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should return expected JSON keys of the pre-packaged rules status', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql([ + 'rules_custom_installed', + 'rules_installed', + 'rules_not_installed', + 'rules_not_updated', + ]); + }); + + it('should return that rules_not_installed are greater than zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_not_installed).to.be.greaterThan(0); + }); + + it('should return that rules_custom_installed, rules_installed, and rules_not_updated are zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_custom_installed).to.eql(0); + expect(body.rules_installed).to.eql(0); + expect(body.rules_not_updated).to.eql(0); + }); + + it('should show that one custom rule is installed when a custom rule is added', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_custom_installed).to.eql(1); + expect(body.rules_installed).to.eql(0); + expect(body.rules_not_updated).to.eql(0); + }); + + it('should show rules are installed when adding pre-packaged rules', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_installed).to.be.greaterThan(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts new file mode 100644 index 0000000000000..ff7a9c259da54 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts @@ -0,0 +1,379 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleAsNdjson, + getSimpleRuleOutput, + removeServerGeneratedProperties, + ruleToNdjson, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('import_rules', () => { + describe('importing rules without an index', () => { + it('should not create a rule if the index does not exist', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(400); + + // We have to wait up to 5 seconds for any unresolved promises to flush + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Try to fetch the rule which should still be a 404 (not found) + const { body } = await supertest.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`).send(); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "rule-1" not found', + }); + }); + + it('should return an error that the index needs to be created before you are able to import a single rule', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + + it('should return an error that the index needs to be created before you are able to import two rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('importing rules with an index', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should set the response content types to be expected', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + }); + + it('should reject with an error if the file type is not that of a ndjson', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.txt') + .expect(400); + + expect(body).to.eql({ + status_code: 400, + message: 'Invalid file extension .txt', + }); + }); + + it('should report that it imported a simple rule successfully', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should report that it failed to import a thousand and one (10001) simple rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(new Array(10001).fill('rule-1')), 'rules.ndjson') + .expect(500); + + expect(body).to.eql({ message: "Can't import more than 10000 rules", status_code: 500 }); + }); + + it('should be able to read an imported rule back out correctly', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1')); + }); + + it('should be able to import two rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 2, + }); + }); + + it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'More than one rule with rule-id: "rule-1" found', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 1, + }); + }); + + it('should NOT report a conflict if there is an attempt to import two rules with the same rule_id and overwrite is set to true', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 0, + }); + }); + + it('should NOT report a conflict if there is an attempt to import a rule with a rule_id that already exists and overwrite is set to true', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should overwrite an existing rule if overwrite is set to true', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + const simpleRule = getSimpleRule('rule-1'); + simpleRule.name = 'some other name'; + const ndjson = ruleToNdjson(simpleRule); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', ndjson, 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + const ruleOutput = getSimpleRuleOutput('rule-1'); + ruleOutput.name = 'some other name'; + ruleOutput.version = 2; + expect(bodyToCompare).to.eql(ruleOutput); + }); + + it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 2, + }); + }); + + it('should report a mix of conflicts and a mix of successes', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'rule_id: "rule-2" already exists', + status_code: 409, + }, + rule_id: 'rule-2', + }, + ], + success: false, + success_count: 1, + }); + }); + + it('should be able to correctly read back a mixed import of different rules even if some cause conflicts', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .expect(200); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .expect(200); + + const { body: bodyOfRule1 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const { body: bodyOfRule2 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-2`) + .send() + .expect(200); + + const { body: bodyOfRule3 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-3`) + .send() + .expect(200); + + const bodyToCompareOfRule1 = removeServerGeneratedProperties(bodyOfRule1); + const bodyToCompareOfRule2 = removeServerGeneratedProperties(bodyOfRule2); + const bodyToCompareOfRule3 = removeServerGeneratedProperties(bodyOfRule3); + + expect([bodyToCompareOfRule1, bodyToCompareOfRule2, bodyToCompareOfRule3]).to.eql([ + getSimpleRuleOutput('rule-1'), + getSimpleRuleOutput('rule-2'), + getSimpleRuleOutput('rule-3'), + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts new file mode 100644 index 0000000000000..917654e50cb99 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.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 { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled', function() { + this.tags('ciGroup1'); + + loadTestFile(require.resolve('./add_prepackaged_rules')); + loadTestFile(require.resolve('./create_rules')); + loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./delete_rules')); + loadTestFile(require.resolve('./delete_rules_bulk')); + loadTestFile(require.resolve('./export_rules')); + loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./find_statuses')); + loadTestFile(require.resolve('./get_prepackaged_rules_status')); + loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./update_rules')); + loadTestFile(require.resolve('./update_rules_bulk')); + loadTestFile(require.resolve('./patch_rules_bulk')); + loadTestFile(require.resolve('./patch_rules')); + loadTestFile(require.resolve('./query_signals')); + loadTestFile(require.resolve('./open_close_signals')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts new file mode 100644 index 0000000000000..2837f2ed52eeb --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/open_close_signals.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteSignalsIndex, + setSignalStatus, + getSignalStatusEmptyResponse, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + + describe('open_close_signals', () => { + describe('validation checks', () => { + it('should not give errors when querying and the signals index does not exist yet', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .send(setSignalStatus({ signalIds: ['123'], status: 'open' })) + .expect(200); + + // remove any server generated items that are indeterministic + delete body.took; + + expect(body).to.eql(getSignalStatusEmptyResponse()); + }); + + it('should not give errors when querying and the signals index does exist and is empty', async () => { + await createSignalsIndex(supertest); + const { body } = await supertest + .post(DETECTION_ENGINE_SIGNALS_STATUS_URL) + .set('kbn-xsrf', 'true') + .send(setSignalStatus({ signalIds: ['123'], status: 'open' })) + .expect(200); + + // remove any server generated items that are indeterministic + delete body.took; + + expect(body).to.eql(getSignalStatusEmptyResponse()); + + await deleteSignalsIndex(supertest); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts new file mode 100644 index 0000000000000..85a4c0f1f664c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleOutputWithoutRuleId, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('patch_rules', () => { + describe('patch rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should patch a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a "403 forbidden" using a rule_id of type "machine learning"', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's type to machine learning + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', type: 'machine_learning' }) + .expect(403); + + expect(body).to.eql({ + message: 'Your license does not support machine learning. Please upgrade your license.', + status_code: 403, + }); + }); + + it('should patch a single rule property of name using the auto-generated rule_id', async () => { + // create a simple rule + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: createRuleBody.rule_id, name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutputWithoutRuleId(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ id: createdBody.id, name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change the version of a rule when it patches only enabled', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', enabled: false }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it patches enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false and another property + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', severity: 'low', enabled: false }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change other properties when it does patches', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's timeline_title + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.timeline_title = 'some title'; + outputRule.timeline_id = 'some id'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should give a 404 if it is given a fake id', async () => { + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ id: 'fake_id', name: 'some other name' }) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should give a 404 if it is given a fake rule_id', async () => { + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'fake_id', name: 'some other name' }) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts new file mode 100644 index 0000000000000..74aea542c32a3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts @@ -0,0 +1,358 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + getSimpleRuleOutputWithoutRuleId, + removeServerGeneratedPropertiesIncludingRuleId, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('patch_rules_bulk', () => { + describe('patch rules bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should patch a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch two rule properties of name using the two rules rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // patch both rule names + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { rule_id: 'rule-1', name: 'some other name' }, + { rule_id: 'rule-2', name: 'some other name' }, + ]) + .expect(200); + + const outputRule1 = getSimpleRuleOutput(); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutput('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); + }); + + it('should patch a single rule property of name using an id', async () => { + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: createRuleBody.id, name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch two rule properties of name using the two rules id', async () => { + // create a simple rule + const { body: createRule1 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + const { body: createRule2 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // patch both rule names + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { id: createRule1.id, name: 'some other name' }, + { id: createRule2.id, name: 'some other name' }, + ]) + .expect(200); + + const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); + }); + + it('should patch a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: createdBody.id, name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change the version of a rule when it patches only enabled', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', enabled: false }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it patches enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false and another property + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', severity: 'low', enabled: false }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change other properties when it does patches', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's timeline_title + await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }]) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.timeline_title = 'some title'; + outputRule.timeline_id = 'some id'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: 'fake_id', name: 'some other name' }]) + .expect(200); + + expect(body).to.eql([ + { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'fake_id', name: 'some other name' }]) + .expect(200); + + expect(body).to.eql([ + { + rule_id: 'fake_id', + error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, + }, + ]); + }); + + it('should patch one rule property and give an error about a second fake rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch one rule name and give a fake id for the second + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { rule_id: 'rule-1', name: 'some other name' }, + { rule_id: 'fake_id', name: 'some other name' }, + ]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should patch one rule property and give an error about a second fake id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch one rule name and give a fake id for the second + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { id: createdBody.id, name: 'some other name' }, + { id: 'fake_id', name: 'some other name' }, + ]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + id: 'fake_id', + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/query_signals.ts b/x-pack/test/detection_engine_api_integration/basic/tests/query_signals.ts new file mode 100644 index 0000000000000..f4e3c2fa2ae1a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/query_signals.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { getSignalStatus, createSignalsIndex, deleteSignalsIndex } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + + describe('query_signals_route', () => { + describe('validation checks', () => { + it('should not give errors when querying and the signals index does not exist yet', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getSignalStatus()) + .expect(200); + + // remove any server generated items that are indeterministic + delete body.took; + + expect(body).to.eql({ + timed_out: false, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + hits: { total: { value: 0, relation: 'eq' }, max_score: 0, hits: [] }, + }); + }); + + it('should not give errors when querying and the signals index does exist and is empty', async () => { + await createSignalsIndex(supertest); + const { body } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getSignalStatus()) + .expect(200); + + // remove any server generated items that are indeterministic + delete body.took; + + expect(body).to.eql({ + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 0, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: { + statuses: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }); + + await deleteSignalsIndex(supertest); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts new file mode 100644 index 0000000000000..51116c6585e7d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('read_rules', () => { + describe('reading rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should be able to read a single rule using rule_id', async () => { + // create a simple rule to read + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should be able to read a single rule using id', async () => { + // create a simple rule to read + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should be able to read a single rule with an auto-generated rule_id', async () => { + // create a simple rule to read + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${createRuleBody.rule_id}`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return 404 if given a fake id', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should return 404 if given a fake rule_id', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts new file mode 100644 index 0000000000000..be64f78f1d2dc --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleOutputWithoutRuleId, + getSimpleMlRule, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('update_rules', () => { + describe('update rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should update a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.rule_id = 'rule-1'; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a 403 forbidden if it is a machine learning job', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's type to try to be a machine learning job type + const updatedRule = getSimpleMlRule('rule-1'); + updatedRule.rule_id = 'rule-1'; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(403); + + expect(body).to.eql({ + message: 'Your license does not support machine learning. Please upgrade your license.', + status_code: 403, + }); + }); + + it('should update a single rule property of name using an auto-generated rule_id', async () => { + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.rule_id = createRuleBody.rule_id; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutputWithoutRuleId(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.name = 'some other name'; + updatedRule.id = createdBody.id; + delete updatedRule.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it updates enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's enabled to false and another property + const updatedRule = getSimpleRule('rule-1'); + updatedRule.severity = 'low'; + updatedRule.enabled = false; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.timeline_title = 'some title'; + ruleUpdate.timeline_id = 'some id'; + + // update a simple rule's timeline_title + await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate) + .expect(200); + + const ruleUpdate2 = getSimpleRule('rule-1'); + ruleUpdate2.name = 'some other name'; + + // update a simple rule's name + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate2) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should give a 404 if it is given a fake id', async () => { + const simpleRule = getSimpleRule(); + simpleRule.id = 'fake_id'; + delete simpleRule.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(simpleRule) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should give a 404 if it is given a fake rule_id', async () => { + const simpleRule = getSimpleRule(); + simpleRule.rule_id = 'fake_id'; + delete simpleRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(simpleRule) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts new file mode 100644 index 0000000000000..4c4dd9c775d8f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts @@ -0,0 +1,388 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + getSimpleRuleOutputWithoutRuleId, + removeServerGeneratedPropertiesIncludingRuleId, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('update_rules_bulk', () => { + describe('update rules bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should update a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const updatedRule = getSimpleRule('rule-1'); + updatedRule.name = 'some other name'; + + // update a simple rule's name + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update two rule properties of name using the two rules rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.name = 'some other name'; + + const updatedRule2 = getSimpleRule('rule-2'); + updatedRule2.name = 'some other name'; + + // update both rule names + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1, updatedRule2]) + .expect(200); + + const outputRule1 = getSimpleRuleOutput(); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutput('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); + }); + + it('should update a single rule property of name using an id', async () => { + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createRuleBody.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update two rule properties of name using the two rules id', async () => { + // create a simple rule + const { body: createRule1 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + const { body: createRule2 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // update both rule names + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createRule1.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const updatedRule2 = getSimpleRule('rule-1'); + updatedRule2.id = createRule2.id; + updatedRule2.name = 'some other name'; + delete updatedRule2.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1, updatedRule2]) + .expect(200); + + const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); + }); + + it('should update a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createdBody.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it updates enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's enabled to false and another property + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.severity = 'low'; + updatedRule1.enabled = false; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's timeline_title + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.timeline_title = 'some title'; + ruleUpdate.timeline_id = 'some id'; + + await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + // update a simple rule's name + const ruleUpdate2 = getSimpleRule('rule-1'); + ruleUpdate2.name = 'some other name'; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.id = 'fake_id'; + delete ruleUpdate.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + expect(body).to.eql([ + { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.rule_id = 'fake_id'; + delete ruleUpdate.id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + expect(body).to.eql([ + { + rule_id: 'fake_id', + error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, + }, + ]); + }); + + it('should update one rule property and give an error about a second fake rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.name = 'some other name'; + delete ruleUpdate.id; + + const ruleUpdate2 = getSimpleRule('fake_id'); + ruleUpdate2.name = 'some other name'; + delete ruleUpdate.id; + + // update one rule name and give a fake id for the second + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate, ruleUpdate2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should update one rule property and give an error about a second fake id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update one rule name and give a fake id for the second + const rule1 = getSimpleRule(); + delete rule1.rule_id; + rule1.id = createdBody.id; + rule1.name = 'some other name'; + + const rule2 = getSimpleRule(); + delete rule2.rule_id; + rule2.id = 'fake_id'; + rule2.name = 'some other name'; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([rule1, rule2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + id: 'fake_id', + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 1e6600c7cd2c0..a2d14820350d9 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -66,7 +66,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ssl, serverArgs: [ `xpack.license.self_generated.type=${license}`, - `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + `xpack.security.enabled=${!disabledPlugins.includes('security')}`, ], }, kbnTestServer: { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts index 602b9929485e0..af5abef22fd0a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from './utils'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 43630d81e64ea..71567ebc01a26 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -20,7 +20,7 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleMlRule, getSimpleMlRuleOutput, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index 7d406777e23f0..4aee1c845aad2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -18,7 +18,7 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts index 4902060f2c6ee..6b4f5956cb6bf 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -18,7 +18,7 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts index 8ddb5f0656019..770df50ebc2e1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -18,7 +18,7 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts index ed1f92457e782..1a22873d752c2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts @@ -16,7 +16,7 @@ import { getSimpleRule, getSimpleRuleOutput, removeServerGeneratedProperties, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts index c0356f877377a..b661e5c56285f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts @@ -17,7 +17,7 @@ import { getSimpleRule, getSimpleRuleOutput, removeServerGeneratedProperties, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts index b4c9632320271..a6c64adc6c461 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts @@ -14,7 +14,7 @@ import { deleteSignalsIndex, deleteAllRulesStatuses, getSimpleRule, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts index a366c04330e9b..2727781d3f103 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts @@ -11,7 +11,12 @@ import { DETECTION_ENGINE_RULES_URL, } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, getSimpleRule } from './utils'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index ac0f51abe1c10..6abac5b90ad00 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -17,7 +17,7 @@ import { getSimpleRuleOutput, removeServerGeneratedProperties, ruleToNdjson, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index 3c8c20646885a..2837f2ed52eeb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -13,7 +13,7 @@ import { deleteSignalsIndex, setSignalStatus, getSignalStatusEmptyResponse, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts index 295bd456eeebf..033c009b59d1e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts @@ -17,7 +17,9 @@ import { removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, -} from './utils'; + getSimpleMlRule, + getSimpleMlRuleOutput, +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -57,6 +59,28 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(outputRule); }); + it('should patch a single rule property of name using a rule_id of type "machine learning"', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleMlRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleMlRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + it('should patch a single rule property of name using the auto-generated rule_id', async () => { // create a simple rule const rule = getSimpleRule('rule-1'); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts index 14c9ca76f6aac..87b1d543864bc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts @@ -17,7 +17,7 @@ import { removeServerGeneratedProperties, getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -92,7 +92,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedProperties(body[0]); const bodyToCompare2 = removeServerGeneratedProperties(body[1]); - expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); }); it('should patch a single rule property of name using an id', async () => { @@ -152,7 +153,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); - expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); }); it('should patch a single rule property of name using the auto-generated id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts index 7c8bd8981db10..f4e3c2fa2ae1a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/query_signals.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../plugins/siem/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { getSignalStatus, createSignalsIndex, deleteSignalsIndex } from './utils'; +import { getSignalStatus, createSignalsIndex, deleteSignalsIndex } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts index 1ae6871348bbb..c4e42c56376a3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts @@ -18,7 +18,7 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts index 42501c005d994..3e1a2382d7e62 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -17,7 +17,9 @@ import { removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, -} from './utils'; + getSimpleMlRule, + getSimpleMlRuleOutput, +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -62,6 +64,33 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(outputRule); }); + it('should update a single rule property of name using a rule_id with a machine learning job', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleMlRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleMlRule('rule-1'); + updatedRule.rule_id = 'rule-1'; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleMlRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + it('should update a single rule property of name using an auto-generated rule_id', async () => { const rule = getSimpleRule('rule-1'); delete rule.rule_id; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts index b7f998d4043f7..27117cfff18ea 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -17,7 +17,7 @@ import { removeServerGeneratedProperties, getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, -} from './utils'; +} from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -98,7 +98,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedProperties(body[0]); const bodyToCompare2 = removeServerGeneratedProperties(body[1]); - expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); }); it('should update a single rule property of name using an id', async () => { @@ -170,7 +171,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); - expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + expect(bodyToCompare1).to.eql(outputRule1); + expect(bodyToCompare2).to.eql(outputRule2); }); it('should update a single rule property of name using the auto-generated id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts similarity index 98% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts rename to x-pack/test/detection_engine_api_integration/utils.ts index 5eabecf96f3e6..85c89cd499eef 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -7,8 +7,8 @@ import { Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; -import { OutputRuleAlertRest } from '../../../../plugins/siem/server/lib/detection_engine/types'; -import { DETECTION_ENGINE_INDEX_URL } from '../../../../plugins/siem/common/constants'; +import { OutputRuleAlertRest } from '../../plugins/siem/server/lib/detection_engine/types'; +import { DETECTION_ENGINE_INDEX_URL } from '../../plugins/siem/common/constants'; /** * This will remove server generated properties such as date times, etc...